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
HighPotential Incorrect Fee DistributionFixed

According to the [Exactly project logic](https://medium.com/@exactly_finance/what-exactly-are-we-building-391d6db2692c), the protocol maintains two pools: smart pool and maturity pool. The `distributeEarningsAccordingly` function is designed to distribute a portion of fee to the smart pool. ```solidity=157 /// @notice Returns what proportional of earnings would amountFunded represent considering suppliedSP the total /// @param earnings amount to be distributed as earnings between the two participants /// @param suppliedSP current supply of the smart pool. /// @param amountFunded amount that will be checked if it came from smart pool or not. function distributeEarningsAccordingly( uint256 earnings, uint256 suppliedSP, uint256 amountFunded ) internal pure returns (uint256 earningsA, uint256 earningsB) { earningsB = earnings.fmul(amountFunded - Math.min(suppliedSP, amountFunded), amountFunded); earningsA = earnings - earningsB; } ``` According to the comment in L159, `suppliedSP` parameter is the current supply of the smart pool. Therefore, the variable `earningsA` may represent the fees that belong to the smart pool and `earningsB` represent the leftover fees. In the function `borrowMP`, the `distributeEarningsAccordingly` function will be invoked to distribute the fees. The two return values (`borrowVars.newUnassignedEarnings`, `borrowVars.earningsSP`) are respectively added to the corresponding values `pool.earningsUnassigned` and `smartPoolEarningsAccumulator`. However, the return value might have been used in the **opposite** way. That is, the **second** return value in L144 `borrowVars.earningsSP` is added to the smart pool earning records, which might conflict with the above understanding on `distributeEarningsAccordingly()` that the **first** return value should represent the fees that belong to the smart pools. ```solidity=144 (borrowVars.newUnassignedEarnings, borrowVars.earningsSP) = PoolLib.distributeEarningsAccordingly( borrowVars.fee, pool.smartPoolBorrowed(), amount ); smartPoolEarningsAccumulator += borrowVars.earningsSP; pool.earningsUnassigned += borrowVars.newUnassignedEarnings; ``` The same issue might exist in the function `withdrawMP`: ```solidity=246 (uint256 earningsUnassigned, uint256 newEarningsSP) = PoolLib.distributeEarningsAccordingly( amount - redeemAmountDiscounted, pool.smartPoolBorrowed(), redeemAmountDiscounted ); pool.earningsUnassigned += earningsUnassigned; earningsSP += newEarningsSP; ``` Therefore, it could lead to incorrect fees distributed to the smart pool.

LowInconsistent Comment and CodeFixed

The comment of the function `hasMaturity` states this function will clean the user's position, which does not match the function implementation. ```solidity=218 /// @dev Cleans user's position from encoded maturity pools /// @param encoded encoded maturity dates where the user borrowed /// @param maturity maturity date function hasMaturity(uint256 encoded, uint256 maturity) internal pure returns (bool) { ```

LowIncorrect error thrownFixed

```solidity if (balance < seizeTokens) revert BalanceExceeded(); ``` Per of the context of the function `liquidate`, and the `if` condition, i.e. `balance < seizeTokens`, the error to be thrown should be `BalanceInsufficient()` instead of `BalanceExceeded()`.

LowMistakenly Transferred ETH Could be Locked in thFixed

```solidity receive() external payable {} ``` The `receive` function allows the contract to receive native tokens (ETH). However, if ETH is mistakenly transferred by anyone to the current contract, it could be locked.

LowPotential Reentrancy IssuesFixed

A reentrancy attack can occur when the contract creates a function that makes an external call to another untrusted contract before resolving any effects. If the attacker can control the untrusted contract, they can make a recursive call back to the original function, repeating interactions that would have otherwise not run after the external call resolved the effects. The `FixedLender` inherits from the `ERC4626` contract, which implementation is unknown. Furthermore, the following functions will invoke corresponding functions inherits from the `ERC4626` contract` and are possibly interact with external assets: - `withdraw()` - `redeem()` - `transfer()` - `transferFrom()` For example, if the attacker is able to successfully trigger `redeem()` invocation inside the `withdraw()` invocation, before contact states are resolved, it could lead to a potential reentrancy attacker loophole.

LowPotential Rounding ErrorFixed

In the `repayMP` function, the following calculation may lead to a [rounding error]((https://docs.soliditylang.org/en/v0.8.1/types.html?highlight=division#division)). ```solidity=303 debtCovered = repayAmount.fmul(repayVars.position.principal + repayVars.position.fee, repayVars.amountOwed); ``` If the rounding error occurs, the `debtCovered` will be smaller than expected. Therefore, after reducing the debt, there could be some leftover in the position amd the following condition in the `if` branch will never be met: ```solidity=343 repayVars.position.reduceProportionally(debtCovered); if (repayVars.position.principal + repayVars.position.fee == 0) { delete mpUserBorrowedAmount[maturity][borrower]; userMpBorrowed[borrower] = userMpBorrowed[borrower].clearMaturity(maturity); } else { // we proportionally reduce the values mpUserBorrowedAmount[maturity][borrower] = repayVars.position; } ``` Hence, as the code in the `if` branch will not be executed due to rounding error, the position will never be fully repaid and closed.

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