Exactly

CertiKFiles named in the report

CertiK reviewed Exactly Protocol (Exactly) on 12 Jun 2022: 12 findings, 0 critical; 6 marked fixed, 6 open.

No open critical findings
Open the reportOpen Exactly Protocol’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings12

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High101
Medium100
Low405
HighCentralization Related RisksOpen

In the contract `FixedLender`, the role `DEFAULT_ADMIN_ROLE` has authority over the following functions: - `setMaxFuturePools` will set the protocol's max future weekly pools for borrowing and lending. - `setAccumulatedEarningsSmoothFactor` will set the factor used when smoothly accruing earnings to the smart pool. the role `PAUSER_ROLE` has authority over the following functions: - `pause` will set the `_pause` state to true in case of emergency - `unpause` will set the `_pause` state to false when the threat is gone Any compromise to the `DEFAULT_ADMIN_ROLE` and `PAUSER_ROLE` accounts may allow a hacker to take advantage of this authority. In the contract `ExactlyOracle`, the role `DEFAULT_ADMIN_ROLE` has authority over the following functions: - `setAssetSources` will set or replace the sources of assets. Any compromise to the `DEFAULT_ADMIN_ROLE` account may allow a hacker to take advantage of this authority. In the contract `Auditor`, the role `DEFAULT_ADMIN_ROLE` has authority over the following functions: - `setOracle` will set Oracle's to be used - `setLiquidationIncentive` will set liquidation incentive for the whole ecosystem - `enableMarket` will enable a certain FixedLender market - `setCollateralFactor` will set the collateral factor for a certain fixedLender - `setMarketBorrowCaps` will set the given borrow caps for the given fixedLender markets Any compromise to the `DEFAULT_ADMIN_ROLE` account may allow a hacker to take advantage of this authority. In the contract `InterestRateModel`, the role `DEFAULT_ADMIN_ROLE` has authority over the following functions: - `setCurveParameters` will update this model's curve parameters. - `setSPFeeRate` will update `spFeeRate` Any compromise to the `DEFAULT_ADMIN_ROLE` account may allow a hacker to take advantage of this authority. In the contract `PoolAccounting`, the role `DEFAULT_ADMIN_ROLE` has authority over the following functions: - `setInterestRateModel` will set the interest rate model to be used by this PoolAccounting - `setPenaltyRate` will set the penalty rate per second. - `setSmartPoolReserveFactor` will set the percentage that represents the smart pool liquidity reserves that can't be borrowed. Any compromise to the `DEFAULT_ADMIN_ROLE` account may allow a hacker to take advantage of this authority.

MediumPotential Revert in `Liquidate`Open

The `liquidate` function is designed to liquidate an uncollateralized position. It will call `auditor.liquidateCalculateSeizeAmount` to get the number of tokens to be liquidated and call `_seize` or `collateralFixedLender.seize` to seize a certain amount of tokens. According to the logic of `liquidateCalculateSeizeAmount` function, we have the following equation: $$seizeTokens = \frac{actualRepayAmount * priceBorrowed * 10^{markets[fixedLenderCollateral].decimals} * liquidationIncentive}{priceCollateral * 10^{markets[fixedLenderBorrowed].decimals} * 1e18}$$ ```solidity function liquidateCalculateSeizeAmount( FixedLender fixedLenderBorrowed, FixedLender fixedLenderCollateral, uint256 actualRepayAmount ) external view override returns (uint256) { // Read oracle prices for borrowed and collateral markets uint256 priceBorrowed = oracle.getAssetPrice(FixedLender(fixedLenderBorrowed).assetSymbol()); uint256 priceCollateral = oracle.getAssetPrice(FixedLender(fixedLenderCollateral).assetSymbol()); uint256 amountInUSD = actualRepayAmount.fmul(priceBorrowed, 10**markets[fixedLenderBorrowed].decimals); // 10**18: usd amount decimals uint256 seizeTokens = amountInUSD.fmul(10**markets[fixedLenderCollateral].decimals, priceCollateral); return seizeTokens.fmul(liquidationIncentive, 1e18); } ``` In order to simplify the calculation, assuming that there are the same decimals and the `liquidationIncentive` is 1e18, so the equation will be : $$seizeTokens = \frac{actualRepayAmount * priceBorrowed}{priceCollateral}$$ When the borrower is delayed, there are more and more penalties and the amount of `actualRepayAmount` will increase. When the `actualRepayAmount` increases, the `seizeTokens` will also increase. If the `seizeTokens` is greater than the balance of the collateral asset, the `liquidate` function will revert: ```solidity uint256 seizeTokens = auditor.liquidateCalculateSeizeAmount(this, collateralFixedLender, assets); // Revert if borrower collateral token balance < seizeTokens (uint256 balance, ) = collateralFixedLender.getAccountSnapshot(borrower, maturity); if (balance < seizeTokens) revert BalanceExceeded(); ``` In this case, no one can liquidate this uncollateralized position anymore.

LowDiscussion on Liquidation ProcessOpen

In the Exactly protocol's liquidation logic, the liquidator could repay the debt of the original borrower to get the original borrow's assets. However, if the liquidation is not processed in time, or there is a sharp price drop on certain asset. It could lead to the borrowed assets' value much larger than the collateral's value. Therefore, the protocol might suffer unexpected loss.

LowIncompatibility With Deflationary TokensOpen

When transferring standard ERC20 deflationary tokens, the input amount may not be equal to the received amount due to the charged transaction fee. For example, if a user stakes 100 deflationary tokens (with a 10% transaction fee) in a `FixedLender`, only 90 tokens actually arrived in the contract. However, the user can still withdraw 100 tokens from the contract, which causes the contract to lose 10 tokens in such a transaction.

LowLack of Input ValidationOpen

The `enableMarket()` enables a certain FixedLender market of the protocol. However, there is no explicit validation in `enableMarket` function to ensure the `collateralFactor` is properly set. ```solidity function enableMarket( FixedLender fixedLender, uint128 collateralFactor, string memory symbol, string memory name, uint8 decimals ) external onlyRole(DEFAULT_ADMIN_ROLE) { ```

LowThird Party DependenciesOpen

The contract is serving as the underlying entity to interact with third-party `chainlink` and `WETH` protocols. The scope of the audit treats 3rd party entities as black boxes and assumes their functional correctness. However, in the real world, 3rd parties can be compromised and this may lead to lost or stolen assets. In addition, upgrades of 3rd parties can possibly create severe impacts. For example, if the price of chainlink oracle is inaccurate, it could lead to the loss of the Exactly protocol. Reference: https://twitter.com/CertiKAlert/status/1524968003049332756

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (11)

  • contracts/PoolAccounting.sol
  • contracts/FixedLenderETHRouter.sol
  • contracts/InterestRateModel.sol
  • contracts/Auditor.sol
  • contracts/interfaces/IInterestRateModel.sol
  • contracts/ExactlyOracle.sol
  • contracts/utils/PoolLib.sol
  • contracts/interfaces/IOracle.sol
  • contracts/interfaces/IAuditor.sol
  • contracts/FixedLender.sol
  • contracts/utils/TSUtils.sol

Part of Exactly Protocol’s security record

Exactly Protocol
Audits8 audits by 3 firmsLast audit Oct 2025, by ABDK Consulting.
Critical findingsNo open critical findingsAcross the 8 reports, 1 critical finding was reported. It is marked fixed by the firm that reported it.
Open Exactly Protocol’s security record

Security firm

CertiK

1,044 projects reviewed · 1,375 reports · latest May 2025

See CertiK’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us