GithubHelp home page GithubHelp logo

compound-finance / comet Goto Github PK

View Code? Open in Web Editor NEW
224.0 224.0 129.0 4.6 MB

An efficient money market protocol for Ethereum and compatible chains (aka Compound III, Compound v3).

License: Other

Solidity 17.20% TypeScript 82.60% JavaScript 0.18% Shell 0.03%

comet's People

Contributors

ajb413 avatar arr00 avatar aryanbhasin avatar beched avatar cwang25 avatar hayesgm avatar jflatow avatar kevincheng96 avatar scott-silver avatar toninorair avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

comet's Issues

[N10] Use of Global imports

Non-explicit imports are used throughout all protocol contracts, which reduces code readability and could lead to conflicts between names defined locally and the ones imported.

The inheritance chains within the protocol are long, and for this, the codebase would benefit from refactoring specifically which definitions are being imported in each contract.

On the principle that clearer code is better code, consider using named import syntax (import {A, B, C} from "X") to explicitly declare which contracts are being imported.

Addresses in `relations.ts` should be case-insensitive

Spider currently expects addresses in relations.ts to be all lower case. We should make it case-insensitive.

e.g.

  '0xDbcd5BafBAA8c1B326f14EC0c8B125DB57A5cC4c': {
    artifact: 'contracts/ERC20.sol:ERC20',
  },

will not work right now

[N11] Potential front-run

The deployAndUpgrade function of the CometProxyAdmin is restricted in access to be called exclusively by the governance. It deploys a new Comet instance through the Configurator and upgrades the implementation address in the proxy.

However, it doesn't call the initializeStorage function of the Comet contract through the proxy, leaving the new implementation not initialized. The function doesn't take any input parameter and it is meant to be called only once without the need to call it again on new implementation upgrades.

Whether the first initialization is performed on a separate transaction or if in the future it will be possible to re-initialize the Comet instance with some input values, any user can front-run any governance attempt to initialize the new deployed Comet.

Consider taking into account that deployAndUpgrade and initializeStorage should be done in one unique transaction to avoid the front-run scenario, especially if input parameters or re-initializations are meant to happen in future developments.

[H01] Locked assets in contracts

Once the protocol is deployed, the Comet and Bulker contracts will be two important pieces of the system. The Comet is the main protocol contract while the Bulker is a useful tool to execute multiple protocol calls into one single transaction.

For different purposes, both the Comet and the Bulker support deposits of ETH into the contract.

In the case of the Comet contract, the delegation toward the CometExt is in the fallback function, which is declared as payable, but there are no parts of both contracts that use them. Moreover, there is no direct way to withdraw any ETH balance present in the contract.

In the case of the Bulker, the contract has a receive payable function to accept ETH which is needed to be used in the ACTION_WITHDRAW_ETH according to the docstrings.
In this case, any ETH sent through the receive function will be again lost in the contract, with no possibility to upgrade to a new contract as the Comet can. The reason is that the ACTION_WITHDRAW_ETH is implemented into the withdrawEthTo function. This function unwraps WETH by sending tokens to the WETH contract and receiving back ETH in the same amount in the Bulker contract which are then sent to the user. In this case, the contract's ETH balance will be untouched as only ETH coming from the unwrap will be used.

Lastly, a separate reference must be done also on ERC20 tokens. The Comet contract has the approveThis function which is enough to let a manager move any ERC20 fund that might get lost in Comet balance. However, this is not the case for the Bulker contract, where also ERC20 tokens might get lost.

Consider establishing mechanisms to avoid such scenarios, as it results in a direct loss of user funds.

`withdrawReserves` doesn't call `accrue`

The withdrawReserves uses the getReserves() function which checks the present value of the totalSupply and totalBorrow.
If the accrue is not called beforehand, the present values might not be accurate and the reserve amount that is withdrawn will also not be accurate.
Since this is an onlyGovernance function this is not a big issue, but if the governance forgets to call accrue before it might cause a problem.

[L01] Everyone can deploy new Comet instances

Each Comet contract implementation that the protocol might decide to create is meant to be deployed in the Configurator contract.

This contract is an intermediate logic to bootstrap the entire protocol configuration and then apply it to a newly deployed Comet contract.

To do this, the Configurator makes use of a CometFactory contract which has a clone function whose sole role is to create a new Comet contract and return its address.

Whenever all protocol parameters are set, the deploy function in the Configurator contract is called. This function calls the clone function in the factory and emits an event to signal that a new Comet is ready with the configuration set so far.

The deploy and clone functions are both public callable functions, so anyone can, at any moment, generate the Comet instance either in the factory, passing arbitrary configurations, or either through the Configurator with the configuration set until that moment.

Is not clear why those functions are public as there is no benefit for users to call them. Moreover, leaving open the door for anyone to deploy instances of Comet with arbitrary configuration or emitting arbitrary events through the Configurator is a concern to take into account as it might be misused by malicious actors trying to create pishing-like attacks.

Consider restricting access to those functions and let them be called only by the governor or consider properly documenting such a design decision, raising awareness over the way it might be misused.

Issue in `updateAssetIn` and `isInAsset`

There is a problem in updateAssetIn with the mask size that is used to update the userBasic[account].assetsIn.

The update is as following:
userBasic[account].assetsIn |= (uint8(1) << assetInfo.offset);

As we can see uint(8) is used to create the matching mask by shifting it by assetInfo.offset to the left.
The problem is that there are more than 8 assets in the system so an 8 bits mask is not enough.

For example, let's assume an asset with assetInfo.offset = 10.
uint8(1) << 10 = 0 so the required mask won't be used and a 0 mask will be used instead.
As a result userBasic[account].assetsIn won't get updated correcly.

The same problem happens in isInAsset when we use an 8 bits mask to get inforamtion from userBasic[account].assetsIn:
return (assetsIn & (uint8(1) << assetOffset) != 0);

Borrowing less than `baseBorrowMin` (through `supply`)

It's possible to borrow less than the baseBorrowMin by borrowing more than the baseBorrowMin and then repaying a portion of it.
For example, assuming baseBorrowMin = 100. A user can borrow 101 and right after repay 2 which leaves him with 99 < baseBorrowMin.

Gas optimizations

  1. userBasic is accessed twice in isBorrowCollateralized(), you can save the struct in a local variable instead of accessing a storage variable twice.
  2. totals.lastAccrualTime = now_; in accrue() can be inside the if (timeElapsed > 0) to save gas
  3. Can rearrange state variables to save gas and bytecode size, for example, uint128 uint128 uint64 unit64 uint64 uin64 is better than uint128 uint64 uint128 uint64 uint64 uint64
  4. don’t need to check seizeAmount > 0 because of isInAssest() in absorbInternal()
  5. In buyCollateral(), the second require can be reduced to reserves < int(targetReserves) (or save targetReserves as int in the first place).
  6. accrue() can be called in absorb() instead of absorbInternal() in every loop iteration

[N01] Anyone can set `baseTrackingIndex`

In the Comet contract, the accrueAccount function is publicly callable and it accrues interest rates and updates tracking indexes on the account specified as the input parameter.

Specifically, the tracking indexes are updated in the updateBasePrincipal function where if newPrincipal >= 0 then the baseTrackingIndex is set to a meaningful value.

So anyone is able to set this value for accounts that do not even exist on the protocol. Even if it is not a security issue on its own, consider restricting this function to users that have a principal strictly greater than zero.

Borrowing less than `baseBorrowMin` (through `transferBase`)

It's possible to borrow less than the baseBorrowMin by borrowing more than the baseBorrowMin and then transfer some base tokens from another account to this, reducing the borrow position.
For example, assuming baseBorrowMin = 100. A user can borrow 101 and right after transfer 2 base tokens from another account to this, which leaves him with 99 < baseBorrowMin.

[N09] Naming issues

To favor explicitness and readability, several parts of the contracts may benefit from better naming. Here are some examples:

  • The name CometProxyAdmin suggests that it is only the admin of the Comet proxy, but in reality, it will also have the same role on the Configurator proxy. Choose another name to avoid confusion.
  • TransparentUpgradeableConfiguratorProxy can be called ConfiguratorProxy. There is no need to use the inherited contract name as a prefix.
  • In CometMath, change InvalidUInt to InvalidUint and toUInt to toUint.
  • rescale and descale have different names but the same value, consider using one variable name scale.

too many errors

Hi there, I'm a getting numerous errors in the following scripts: OnChainLiquidator.sol, LiquidateUnderWaterBorrowers.ts, and index.ts.
The main issues are VS Code can't find specific modules, especially "from build/types" in the OnChainLiquidator smart contract, the errors are multiple "Expected indentation of 4 spaces but found 32 [indent]" , multiple "line 33:68 extraneous input ')' expecting {';', '='} and " line 63:29 extraneous input 'balancerVault' expecting {';', '='}". If anyone can help me with these and re-format the scripts and fix all of the issues, then that would be very helpful. Thank you.

[L06] Potential function clashes

The TransparentUpgradeableConfiguratorProxy overrides the _beforeCallback function.

Concretely, the _beforeCallback function is implemented in the TransparentUpgradeableProxy contract to avoid the admin of the contract to call the implementation logic directly.

This feature is removed in TransparentUpgradeableConfiguratorProxy contract, where the admin is allowed now to call directly the implementation by triggering the fallback function as a normal user would do. This is needed because of the deployAndUpgrade function of the CometProxyAdmin which sees the admin calling the Configurator.deploy function.

This is not a security issue on its own but it opens the door for potential clashes to happen. If one function is added either on the proxy or the logic contract, this can clash with any of the other contract functions. At this point, the admin will stop to be able to call the implementation contract (users will still be directed toward the implementation because of the ifAdmin modifier).

This article specifies how crafting clashes may not be too hard of a computational task. This article showcases how a new function in the proxy might actually enable clashes.

To avoid any unwanted behaviors, consider ensuring that the upgrade mechanism for Configurator always checks for potential clashes between the logic implementation and the proxy, especially if new functions are added.

[L05] Missing validations

There are some places in the code base that might benefit from some sanity checks on the input provided:

To reduce possible errors and make the code more rodust, consider adding sanity checks where needed.

Shouldn't `accrue` be called before `isLiquidatable` check in `absorbInternal`?

I think that in the function absorbInternal() the accrue() should be called before the isLiquidatble() check and not after:

require(isLiquidatable(account), "account is not underwater");

        TotalsBasic memory totals = totalsBasic;
        totals = accrue(totals);

That's because the accrue function updates the indexes which need to be updated for the isLiquidatble() check.
For example it is possible that a isLiquidatable check will return false for a certain user but after the accrue() it'll reutrn true, causing the absorbInternal call to return without liquidating when actually it shouldn't.

what do you think?

[L03] Inconvenient use of `immutable` keyword

For accounts that use the protocol, the protocol provides a built-in system for tracking rewards. Comet contracts keep account of all accrued incentives for suppliers and borrowers of the base token, and users can claim them on CometRewards contract. It should be emphasized that all rewards from all the Comets from the same chain are claimed in the same CometRewards contract.

The latter has a governor variable defined which is the role that can set the reward settings and withdraw rewards from the contract. Since the contract does not have an upgradeability mechanism, it is inconvenient to define this variable as immutable. If the governor needs to be changed, a new contract must be deployed and the old contract's state must be migrated to the new one.

Consider adding this variable to the contract storage and specifying a setter function so that the governor can be changed any time it is needed.

Missing events

As pointed out by spencerperkins on Discord, Comet and CometRewards is missing some events that make off-chain indexing difficult:

  • TransferBase event in Comet
  • RewardClaimed event is missing a parameter that specifies which comet the reward is claimed for

[N02] Inconsistent coding style

Inconsistencies in coding style were identified throughout the code base. Some examples are:

  • constants variables version and name and not in UPPER_CASE like other constant variables
  • The convention of functions named with the "_" prefix is not clear. Sometimes it is used for admin functions and other times for internal functions
  • Some structs use _reserved to fill slots, but others do not.
  • It is not clear what convention is used for the naming of codebase interfaces. Sometimes the letter I is used as a prefix, sometimes the word Interface at the end or in some cases the contract even misses both.

Taking into consideration how much value a consistent coding style adds to the project’s readability, enforcing a standard coding style with help of linter tools such as Solhint is recommended.

[M01] `governor` can approve anyone to transfer the base and collateral assets within the Comet contract

The COMP token, the governance module (GovernorBravo) and the Timelock are the three components that make up Compound governance, allowing for the management and upgrade of the protocol.

The Timelock contract, which is also the administrator of the Compound v2 protocol, is meant to be in charge of all instances of the Comet protocol. Each proxy, the Configurator implementation, the Comet factory, the CometRewards and the Comet implementations are all under the governance control.

Within the Comet implementation, there is a variable called governor that will be set to be the Timelock address. The contract also has a function called approveThis that only the governor can execute and it approves anyone to transfer the base and collateral assets outside the Comet contract. Taking into account that the possibility of a governance attack exists (Beanstalk and Curve cases), user funds could be at risk.

If the function is meant to be used for specific purposes and can't be removed, consider properly documenting this risk and establish verification measures on governance proposals to avoid misuse of this feature. To reduce the possibilities of a governance attack, be sure to always rely on a delay mechanism for proposals to be executed and eventually a pause guardian that can take action if a malicious proposal is spotted.

BulkerScenario.ts should bump supply caps in testing

Scenarios:

  • 'Comet#bulker > (non-WETH base) all non-reward actions in one txn'
  • 'Comet#bulker > (non-WETH base) all actions in one txn'

Expectation:
The test shall change supply cap dynamically via executing bumpSupplyCaps if needed to make the supply cap fit to the scenarios.

Actual:
The test will fail if supply cap is to low in configuration.

[L07] Underwater accounts can minimize losses

In the Comet contract, during an absorbeInternal call, the updateBasePrincipal function is called, updating the accrued interests on the liquidated user position.

If the seizing of collateral brought the new user's balance to positive values the user will:

  • Have his excess collateral exchanged for the base asset (during an absorb this might be beneficial if the collateral price is crashing).
  • Have supply interest and tracking indexes accrued straight away after the absorb, over the excess collateral converted into the base asset. Concretely, the accrueInternal will update interest rates and the updateBasePrincipal will update the tracking indexes.

Moreover, an underwater can decide to even liquidate himself, earning additional liquidation points.

Consider the extra value that an underwater can extract from his position to determine if this can be leveraged to create some attacks. If those are intended behaviors, consider improving the docstrings around the absorption mechanism to reflect these details.

[M02] <a name="targetreserveissue"></a>

The protocol may end up holding collateral assets in an unwanted manner

The Comet contract has an immutable value that defines the target amount of reserves of the base token. This value is closely related to the buyCollateral function. This function cannot be called successful if the protocol reserves are greater than or equal to this target.

If targetReserves is set to a small value, the contract could easily reach the level. The problem is that the absorptions can continue but the protocol will not be able to sell the collateral because the buyCollateral function cannot be used and the protocol could be in a situation where it would hold assets that may lose value over time.

In the opposite case, where targetReserves is set to a large value, the chance of reaching this level would be much lower so it could be a useless constraint.

Keeping in mind that setting this variable to a small value is more of a problem, be sure to set it to a large value. Also if the value of the target is too high to not have a useful or practical use, consider re-design the system to not make use of it.

[L08] Unnecessary complexity

The Comet contract delegates some feature implementations into the CometExt contract. This contract is meant to be an actual extension of Comet logic and implement some functions and parameters, including the version parameter.

At the same time, Comet is built through an upgradeable proxy pattern that requires a new Comet version to be deployed and the proxy pointed toward the new implementation.

Doing an upgrade is convenient to upgrade the version number to a greater value and to do so the protocol must deploy also a new CometExt implementation contract to indicate a new version.

This is because the version parameter is declared as constant and can't be changed as a result of an upgrade mechanism without actually changing it in the contract's code.

In the worst-case scenario, an error is performed in the upgrade mechanism, the version number is not incremented and potential systems integrated with the protocol might depend on that version number to establish some other logic.

To avoid deploying a new CometExt even if its implementation didn't change, consider moving the version parameter into the Comet contract code.

Eventually consider keeping version in both contracts to differentiate between them, as both contracts might require some implementation changes.

[L04] Logic contracts initialization allowed

The Configurator contract has an initializable function that is meant to be called by the proxy. However, nothing prevents users from directly calling the initialize function on the contract implementantion. If someone does so, the state of the implementation would be initialized to some meaningful value.

We suggest adding a constructor that sets the version to an incredibly high number so that any attempt to call the implementation at the initialize function would revert with an AlreadyInitialized error or even a specific one to signal that initializing the implementation is prohibited.

Leaving an implementation contract not initialized is generally an insecure pattern to follow. In this case, it might lead to a situation where an attacker can take control over the implementation contract by passing himself as governor and try to mislead users toward malicious contracts. This is not a security issue on its own but we strongly suggest avoiding such scenarios in all implementation contracts.

As a source of inspiration, here there's an example of how the scenario can be avoided in general situations.

[N13] Repetitive code

In many parts of the codebase, repeated or similar lines of code can be seen. Here are some examples:

Consider reusing the same code defined in just one place or, if appropriate, removing duplicate code.

Goland SDK client

What if we develop and add such a client? I think I can develop and open-source such a client by myself.

`isInAsset` might return true even if collateral balance is 0

in absorbInternal you set the user's collateral to 0 but you don't call updateAssetsIn().
this means that the collateral balance of a user is 0, but the bit is still on and the function isInAsset() will return true.
We can't find any major problems that this cause but it is an incorrect behavior of the function and also a waste of gas in all the functions that go over all the collaterals of the user

[N16] Lack of explicitness on data type sizes

The protocol heavily relies on different sized variables, which can have also positive or negative values and different scaling factors. Thanks to this, the deployment and execution of the codebase will decrease gas costs.

Given the fact that it adds some complexity and undermines the readability of the codebase, it is of utter importance to maintain explicitness and consistency across different contracts.

Specifically, implicit castings and sizes should be avoided.

To improve the overall quality of the codebase, consider reviewing it in its entirety, changing all occurrences of uint to uint256 and of int to int256. Consider also reviewing implicit castings from small to bigger sizes and always use the appropriate size for each variable.

[N14] Typos

Line 96 of the CometExt contract has a typo "whi" and the number of decimals showed here is wrong, as it should be one zero less.

To improve correctness and readability, consider reviewing both the contracts and the documentation for typos.

Gas optimization and Code size

Gas optimization:

  1. in withdrawCollateralyou can replace saving totalsCollateral in a local variable since you accesse it only once
  2. Safe64 on asset.scale in isBorrowCollateralized and getBorrowLiquidity are redundant since they are already uint64
  3. Can replace unsigned104 with unit104 in: presentValue(), prinicipalValue(), repayAndSupplyAmount(), withdrawAndBorrowAmount() 
and in absorbInternal() (-oldBalance and newBalance >= 0)
  4. In absorbInternal, can replace 
newBalance = newBalance < 0 ? int104(0) : newBalance;
    with 
if(newBalance < 0) { newBalance = 0;}

Code Size:

  1. The functions isBorrowCollateralized, getBorrowLiquidity, isLiquidatable, getLiquidationMargin use a lot of the same code, if you want to save code size over gas optimization you can merge them
  2. in updateBaseBalance() you can replace:
        if (principal >= 0) {
            uint indexDelta = totals.trackingSupplyIndex - basic.baseTrackingIndex;
            basic.baseTrackingAccrued += safe64(uint104(principal) * indexDelta / baseIndexScale); // XXX decimals
        } else {
            uint indexDelta = totals.trackingBorrowIndex - basic.baseTrackingIndex;
            basic.baseTrackingAccrued += safe64(uint104(-principal) * indexDelta / baseIndexScale); // XXX decimals
        }

with:

        uint indexDelta;
        if (principal >= 0) {
              indexDelta = totals.trackingSupplyIndex - basic.baseTrackingIndex;
         } else {
              indexDelta = totals.trackingBorrowIndex - basic.baseTrackingIndex;
              principal = -principal;
         }
         basic.baseTrackingAccrued += safe64(uint104(principal) * indexDelta / baseIndexScale);

if you prefer code size over gas optimization

`SupplyRate > BorrowRate` in some cases where `utilizationRatio > 1`

SupplyRate and BorrowRate are being calculated as following:

function getSupplyRate() public view returns (uint64) {
        uint utilization = getUtilization();
        uint reserveScalingFactor = utilization * (FACTOR_SCALE - reserveRate) / FACTOR_SCALE;
        if (utilization <= kink) {
            // (interestRateBase + interestRateSlopeLow * utilization) * utilization * (1 - reserveRate)
            return safe64(mulFactor(reserveScalingFactor, (perSecondInterestRateBase + mulFactor(perSecondInterestRateSlopeLow, utilization))));
        } else {
            // (interestRateBase + interestRateSlopeLow * kink + interestRateSlopeHigh * (utilization - kink)) * utilization * (1 - reserveRate)
            return safe64(mulFactor(reserveScalingFactor, (perSecondInterestRateBase + mulFactor(perSecondInterestRateSlopeLow, kink) + mulFactor(perSecondInterestRateSlopeHigh, (utilization - kink)))));
        }
    }

function getBorrowRate() public view returns (uint64) {
        uint utilization = getUtilization();
        if (utilization <= kink) {
            // interestRateBase + interestRateSlopeLow * utilization
            return safe64(perSecondInterestRateBase + mulFactor(perSecondInterestRateSlopeLow, utilization));
        } else {
            // interestRateBase + interestRateSlopeLow * kink + interestRateSlopeHigh * (utilization - kink)
            return safe64(perSecondInterestRateBase + mulFactor(perSecondInterestRateSlopeLow, kink) + mulFactor(perSecondInterestRateSlopeHigh, (utilization - kink)));
        }
    }

Note that getBorrowRate() * utilization * (1 - reserveRate) = getSupplyRate(),
That is because the protocol wants to keep part of the borrow rate to the reserves and not to give it all to the suppliers.
The problem is that in some cases where utilization > 1 , utilization * (1 - reserveRate) is greater than 1 and then getSupplyRate() > getBorrowRate() and all the borrowRate income will go to the suppliers and the difference between the interestRate for the suppliers to this income would come from the reserves.

We made a graph in demos that shows what is this transition point (utilization ratio) where getSupplyRate() > getBorrowRate().
We restricted the kink to be lower than 1 (k), the percentage that goes to the reserves to be lower than 1 (P), and the high slope to be GE to the low slope (i think this is a reasonable assumption). You can play with it and try out concrete values and limits.
https://www.desmos.com/calculator/x6onz1rrpt

We also came to the conclusion that this transition point where getSupplyRate() > getBorrowRate() is = 1/(1-p) where p is the reserveRate.

[N12] Potential reentrancies

In the codebase, we found two places where reentrancy can occur. However, those do not pose any security issue or concern but awareness should be raised:

  • The invoke function of the Bulker contract can be re-entered.
  • The doTransferIn function of the Comet contract is often executed at the very beginning of executions, being it an anti-pattern to follow against reentrancy.

To improve clarity, consider either reducing the attack surface by making those functions non-reentrant or clearly stating this risk in the docstrings.

[N08] `rewardsClaimed` can be mixed between different tokens

In CometRewards it is possible to change the reward token of each Comet through _setRewardConfig. However, if the reward token is changed, the number of previous reward tokens claimed will persist and once someone claims their new reward asset, it will be added to rewardsClaimed despite being different assets.

Consider removing the ability to change the reward asset once set or changing the way the claimed rewards are stored if the reward asset changes.

How much will the points from the absorb be worth and how big will the discount be in `buyCollateral`?

If there might be a case of a user being alone in the system at the beginning with no initial supply, this user can easily force himself into liquidation and get as many points as he wants.

This can be accomplished like this:

  1. I transfer some amount of money to the system, for example, 100$ = 10e8 tokens
  2. I supply 1 token to the system, now the utilization is 10e8/1 = 10e8
  3. I create many users and borrow a small amount, like 10e4 tokens each, I can do 10,000 of those
  4. since the utilization is huge, it won't take long until they are all in liquidation, and then I go over everyone and absorb them

If the points are worth more than the total gas of this attack, I will make a profit.
besides the points I earn from the absorb, the collateral I supplied for the 10,000 users is now on discount, and if I can buy it for less than 100$, that my 10,000 users borrowed, I made a profit

[N07] Contracts folder is not properly organized

There is no convenient structure in the contracts folder to easily navigate between them. All contracts, regardless of their type or module they belong to, are mixed in a single folder.

To favor the developer experience and the maintenance of the codebase, consider adding additional folders following the structure within the vendor directory or separating protocol components into different internal folders.

[N15] Unnecessary return values

In the Comet contract, there are functions like the supply or withdraw that return calls to internal functions even if there's no actual final value returned at the end.

Consider reviewing all the occurrences where this happens and avoid returning when not necessary. This will improve readability and correctness.

[N17] `PRICE_SCALE` constant is not used

A constant called PRICE_SCALE is defined in the CometCore contract and is supposed to be used to scale the prices returned by Chainlink aggregators.

Although the function priceScale returns the value stored in the bytecode, the constant is not used anywhere else.

Consider removing this constant or alternatively integrating it into the codebase.

[N05] Incorrect or missing docstrings

Across the codebase, some contracts lack proper documentation and docstrings. In particular, the IWETH9 and CometMath functions or the CometConfiguration and CometStorage variables are having little comments or nothing at all.

Consider thoroughly documenting all functions and parameters that are part of the contracts' public API. Functions or parameters implementing sensitive functionality, even if not public, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).

Moreover, the Configurator contract explicitly states that BaseToken and TrackingIndexScale have no setters but it doesn't specify where those are set, eventually in the config parameter passed as input in the Configurator initialization.

Consider writing this in the docstrings of the initialize function to improve clarity.

[L02] Gas inefficiencies

There are many places throughout the codebase where changes can be made to improve gas consumption. For example:

  • The for loops inside absorb and invoke functions do not cache the length of the array and perform unnecessary operations on each iteration.
  • The unchecked blocks for loop counters are not used consistently throughout the codebase. It is only used in the Invoke function, although it could be implemented in loops of the rest of the protocol contracts.
  • updateBasePrincipal in Comet should not check principal = 0 because in that case nothing will happen and gas will be wasted.
  • It is advisable that if a variable from the contract's storage is going to be read within a function several times, a copy in memory should first be created since reading to the storage directly is expensive. However, if you are only querying for the value once, as in accrueInternal and Comet's fallback, it is recommended to read directly from storage without any intermediate steps.
  • In transferCollateral within the Comet contract, it is recommended to consult with getAssetInfoByAddress at the beginning of the function so that in case the token passed by the parameters is not within the collaterals of the protocol, the function fails early and without wasting gas unnecessarily.
  • Use unchecked { ++i; } over unchecked { i++; }.

When performing these changes, aim to reach an optimal tradeoff between gas optimization and readability. Having a codebase that is easy to understand reduces the chance of errors in the future and improves transparency for the community.

[M03] Incorrect accounting of used gas

The asborbInternal function of the Comet contract contains important logic of the protocol where users that are liquidatable have their debts absorbed by the protocol. To do this task frequently and maintain health in the system, users will call this function whenever they detect a liquidatable user position.

As a reward for doing this task recurrently, absorbers (users calling the absorb function) are accounted for their gas expenditure into liquidation points, with the promise of redeeming those points for reward tokens in a separate contract.

The reward mechanism for the liquidation points is out of the scope for the current audit so we can't assess the incentives alignments in performing this task with profitable rewards.

However, how the gas used is measured doesn't reflect entirely the actual transaction cost for the user. In particular:

  • The priority fee is not taken into account. Absorbers will probably have to compete with each other and be as fast as possible in running absorbs. For this is likely to have a priority fee set as a miner's tip in the transaction. Currently, the protocol only uses block.basefee but tx.gasprice = block.basefee + priority fee should be used instead.
  • Other operations are performed after the gas spent is measured, consuming more gas which is not taken into account.
  • Potentially, a user could deposit a minimum amount of each of the collateral assets supported by the protocol to increase the cost of the transaction, since it implies iteration over all supported assets recursively. Doing so, the rewards increase proportionally to the cost an underwater account might even absorb its own position to earn rewards and potentially reduce damage from the liquidation.

Consider taking these suggestions into account and changing the way the gas used is measured to improve transparency and design correctness.

[N18] Wrong value emitted in event

Lines 1219-1221 of the asborbInternal function in the Comet contract are:

uint104 debtAbsorbed = unsigned104(newBalance - oldBalance);
uint valueOfDebtAbsorbed = mulPrice(debtAbsorbed, basePrice, uint64(baseScale));
emit AbsorbDebt(absorber, account, debtAbsorbed, valueOfDebtAbsorbed);

Where, during an absorb oldBalance is negative (otherwise the isLiquidatable modifier at the beginning of the absorb function would have exited earlier) and newBalance >=0. But if newBalance > 0 the debtAbsorbed should be unsigned104(-oldBalance - newBalance) instead since the positive excess is not actual debt that has been absorbed.

Consider emitting the correct value in the AsborbDebt event or renaming the debtAbsorbed variable to reflect that it does not account only for the debt absorbed but also the excess collateral exchanged for the base asset.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.