CertiK reviewed Exactly Protocol (Exactly) on 12 Jun 2022: 12 findings, 0 critical; 6 marked fixed, 6 open.
Report hosted on skynet.certik.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 1 | 0 | 1 |
| Medium | 1 | 0 | 0 |
| Low | 4 | 0 | 5 |
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.
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) { ```
```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()`.
```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.
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.
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.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (11)
1,044 projects reviewed · 1,375 reports · latest May 2025
See CertiK’s auditsSomething 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