As of 3 Nov 2026, Nova Bank has 1 audit report on Trustblock, by CertiK, dated 13 Nov 2024. That report lists no open critical findings.
Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.
Newest first
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 1 | 0 | 0 |
| Medium | 8 | 0 | 0 |
| Low | 10 | 0 | 0 |
In the contract `BondDepositoryDai`, the role `_owner` has authority over the functions shown in the diagram below. Any compromise to the `_owner` account may allow the hacker to take advantage of this authority and initialize bond terms with provided parameters, set the adjustment settings, set bond terms based on parameters, set the contract address based on ID, set the required stake amount, and set the staking address using the helper option.  In the contract `Ownable` the role `_newOwner` has authority over the function shown in the diagram below. Any compromise to the `_newOwner` account may allow the hacker to take advantage of this authority and retrieve contract ownership, **even if the ownership has been renounced**.  In the contract `Ownable` the role `_owner` has authority over the functions shown in the diagram below. Any compromise to the `_owner` account may allow the hacker to take advantage of this authority and renounce/transfer contract ownership to arbitrary accounts.  In the contract `Ownable`, the role `_manager` has authority over the functions shown in the diagram below. Any compromise to the `_manager` account may allow the hacker to take advantage of this authority and push new ownership to a new owner address, or renounce management rights.  In the contract `ERC20TokenX`, the role `DEFAULT_ADMIN_ROLE` has authority over the functions shown in the diagram below. Any compromise to the `DEFAULT_ADMIN_ROLE` account may allow the hacker to take advantage of this authority and set the buy or sell fee ratio, set the main pair address.  In the contract `ERC20TokenX`, the role `MINT` has authority over the function shown in the diagram below. Any compromise to the `MINT` account may allow the hacker to take advantage of this authority and mint tokens to a specified account.  In the contract `Distributor`, the role `_owner` has authority over the functions shown in the diagram below. Any compromise to the `_owner` account may allow the hacker to take advantage of this authority and add a reward recipient, set adjustment parameters, or remove a recipient from the list.  In the contract `Distributor`, the role `_policy` has authority over the functions shown in the diagram below. Any compromise to the `_policy` account may allow the hacker to take advantage of this authority and set adjustment parameters at specified index, remove recipient by verifying with index and address, and add a reward recipient.  In the contract `StakingV2`, the role `_manager` has authority over the functions shown in the diagram below. Any compromise to the `_manager` account may allow the hacker to take advantage of this authority and set the warmup period, as well as set specified contract to a given address.  In the contract `StakingV2`, the role `locker` has authority over the functions shown in the diagram below. Any compromise to the `locker` account may allow the hacker to take advantage of this authority and **transfer arbitrary amounts of sOHM out of the staking contract as the LockBonus**.  In the contract `Treasury`, the role `_manager` has authority over the functions shown in the diagram below. Any compromise to the `_manager` account may allow the hacker to take advantage of this authority and toggle access and update roles based on managing parameter, queue changes for different managing addresses, audit and update total reserves. ; ``` However, totalDebt is incremented in the same `deposit` function, and `totalDebt + value` could exceed `maxDebt` after the `deposit` function is executed. ```solidity totalDebt = totalDebt.add( value );
Open the reportThe public `claim()` function and `redeem()` function in the linked contracts accept an arbitrary `_recipient` address parameter, rather than using `msg.sender`. This design allows any user to call `claim()`/`redeem()` for another account without the account holder's consent. While this could be by design, to facilitate claims through the `StakingHelper` contract on behalf of users, it also opens the possibility for unauthorized claims on other users' behalf. We would like to confirm if this behavior is intentional.
Open the reportWhen transferring deflationary ERC20 tokens, the input amount may not be equal to the received amount due to the charged transaction fee. For example, if a user sends 100 deflationary tokens (with a 10% transaction fee), only 90 tokens actually arrive to the contract. However, a failure to discount such fees may allow the same user to withdraw 100 tokens from the contract, which causes the contract to lose 10 tokens in such a transaction. In BondV2.sol ```solidity=1159 IERC20(principle).safeTransferFrom(msg.sender, address(this), _amount); IERC20(principle).approve(address(treasury), _amount); ``` In Treasury.sol: ```solidity=399 IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); ```
Open the reportThe `distribute()` function in the `Distributor` contract calculates minting amounts for OHM tokens based on the current `totalSupply`, applying a rate (`_rate`) to determine the reward for each recipient. Since `totalSupply` increases with each minting operation within the same distribution cycle, recipients listed later in the iteration receive more rewards due to the inflated supply. This sequential dependency can lead to unintended distribution of rewards and potentially exceed predetermined minting limits. ```solidity function distribute() external returns ( bool ) { if ( nextEpochBlock <= block.number ) { nextEpochBlock = nextEpochBlock.add( epochLength ); // set next epoch block // distribute rewards to each recipient for ( uint i = 0; i < info.length; i++ ) { if ( info[ i ].rate > 0 ) { ITreasury( treasury ).mintRewards( // mint and send from treasury info[ i ].recipient, nextRewardAt( info[ i ].rate ) ); adjust( i ); // check for adjustment } } return true; } else { return false; } } ``` ```solidity function nextRewardAt( uint _rate ) public view returns ( uint ) { return IERC20( OHM ).totalSupply().mul( _rate ).div( 1000000 ); } ```
Open the reportFunction `setContract` allows the contract owner (referred to as the "manager") to set various critical contract addresses, including the locker address. ```solidity=773 function setContract( CONTRACTS _contract, address _address ) external onlyManager() { if( _contract == CONTRACTS.DISTRIBUTOR ) { // 0 distributor = _address; } else if ( _contract == CONTRACTS.WARMUP ) { // 1 require( warmupContract == address( 0 ), "Warmup cannot be set more than once" ); warmupContract = _address; } else if ( _contract == CONTRACTS.LOCKER ) { // 2 locker = _address; } else if(_contract == CONTRACTS.RELEASE){ require( _address != address(0), "ZeroAddress" ); releasePool = _address; } ``` The function lacks proper validation when setting the locker address. Specifically, there are no checks to prevent the owner from changing the locker address multiple times or setting it to an invalid or malicious address. The malicious locker address could siphon `sOHM` tokens from the staking contract, and gain extra profit through warm up reward claiming.
Open the reportThe `BondV2` contract interacts with out-of-scope dependencies including the `stakingHelper` contract. The out of scope contracts get OHM token approval from the bond contract. The scope of the audit treats out-of-scope dependencies as black boxes and assumes their functional correctness. ```solidity=1311 if (useHelper) { // use if staking warmup is 0 IERC20(OHM).approve(stakingHelper, _amount); IStakingHelper(stakingHelper).stake(_amount, _recipient); ``` The `StakingV2` contract interacts with out-of-scope dependencies including the `releasePool` contract. The out of scope contracts get OHM token approval from the staking contract. The scope of the audit treats out-of-scope dependencies as black boxes and assumes their functional correctness. ```solidity=699 IERC20(OHM).safeApprove(releasePool, _amount); IReleasePool(releasePool).startRelease(staker, _amount, burnAmt, releaseLevel); ``` The linked contracts grant large token allowances to external contracts (`stakingHelper` and `releasePool`) without resetting the allowance afterward. If these external contracts are malicious or compromised, they can transfer an unlimited amount of OHM tokens from the `BondV2` and `StakingV2` contracts. The `stakingHelper` and `releasePool` contracts are not included in the scope of the audit and are treated as black boxes. Without auditing these dependencies, their security cannot be guaranteed.
Open the reportAfter ownership is renounced, the potential for the original owner to regain ownership exists due to the contract code not resetting the state variable for the new owner candidate to a default value. This situation arises because the function to renounce ownership does not properly clear or update the related state variable. This allows the original owner to exploit this vulnerability under certain conditions, bypassing security checks and regaining ownership of the contract by calling the relevant function again.
Open the reportThere is no upper limit restricting parameter of `setAdjustment()`, potentially enabling arbitrary increments on control variables. The commented-out requirement line suggests an intended check against the `terms.controlVariable` to restrict the increment size, which is important for maintaining controlled adjustments. ```solidity=1024 function setAdjustment( bool _addition, uint _increment, uint _target, uint _buffer ) external onlyPolicy { // require( // _increment <= terms.controlVariable.mul(25).div(1000), // "Increment too large" // ); adjustment = Adjust({ add: _addition, rate: _increment, target: _target, buffer: _buffer, lastBlock: block.number }); } ``` Besides, the `BondDepository.sol` contract's design permits the potential for bond price manipulation through the unregulated adjustment of the `terms.controlVariable` via the `adjust()` function. This function dynamically alters the controlVariable based on the `adjustment.add` boolean flag—incrementing it when TRUE and decrementing it when FALSE. ---------------------------------------------------------------------- Here are the code blocks that related to the bond price calculation: ```solidity function deposit( uint _amount, uint _maxPrice, address _depositor ) external returns ( uint ) { require( _depositor != address(0), "Invalid address" ); decayDebt(); ...... uint value = ITreasury( treasury ).valueOf( principle, _amount ); uint payout = payoutFor( value ); ...... adjust(); //**COMMENT from CertiK: This function is used to modify the value of variable terms.controlVariable according to the value of adjustment.add. When adjustment.add is TRUE, terms.controlVariable increases each time, otherwise terms.controlVariable decreases each time.** ``` ```solidity function adjust() internal { uint blockCanAdjust = adjustment.lastBlock.add( adjustment.buffer ); if(adjustment.rate != 0 && block.number >= blockCanAdjust) { uint initial = terms.controlVariable; if (adjustment.add) { terms.controlVariable = terms.controlVariable.add( adjustment.rate ); if (terms.controlVariable >= adjustment.target) { adjustment.rate = 0; } } else { terms.controlVariable = terms.controlVariable.sub(adjustment.rate); if (terms.controlVariable <= adjustment.target) { adjustment.rate = 0; } } adjustment.lastBlock = block.number; emit ControlVariableAdjustment(initial, terms.controlVariable, adjustment.rate, adjustment.add); } } ``` ```solidity function decayDebt() internal { totalDebt = totalDebt.sub( debtDecay() ); lastDecay = block.number; } function payoutFor( uint _value ) public view returns ( uint ) { return FixedPoint.fraction( _value, bondPrice() ).decode112with18().div( 1e16 ); } function bondPrice() public view returns ( uint price_ ) { price_ = terms.controlVariable.mul(debtRatio()).add(1000000000).div( 1e7 ); if ( price_ < terms.minimumPrice ) { price_ = terms.minimumPrice; } } function debtRatio() public view returns ( uint debtRatio_ ) { uint supply = IERC20(OHM).totalSupply(); debtRatio_ = FixedPoint.fraction( currentDebt().mul(1e9), supply ).decode112with18().div(1e18); } function currentDebt() public view returns (uint) { return totalDebt.sub(debtDecay()); } function debtDecay() public view returns (uint decay_) { uint blocksSinceLast = block.number.sub(lastDecay); decay_ = totalDebt.mul(blocksSinceLast).div(terms.vestingTerm); if (decay_ > totalDebt) { decay_ = totalDebt; } } ``` In the Treasury.sol contract: ```solidity function valueOf(address _token, unit _amount) public view returns (uint value_) { if (isReserveToken[_token]) { // convert amount to match LMDA decimals value_ = _amount.mul(10**IERC20(LMDA).decimals()).div(10**IERC20(_token).decimals()); } else if (isLiquidityToken[_token]) { value_ = IBondCalculator(bondCalculator[_token]).valuation(_token, _amount); } } ``` Referring to the above code, the bond price can be expressed as the following formulas: $$ \begin{align} bondAmount &= \frac{Value}{bondPrice \times 10^{16}} && \tag 1\\ bondPrice &= controlVariable \times \frac{totalDebt'}{totalSupply \times 10^9} + 100 && \tag 2\\ totalDebt' &= totalDebt \times \left( 1 - \frac{\Delta block}{vestingTerm} \right) && \tag 3\\ where \ \ & \Delta block \leq vestingTerm && \notag\ \end{align} $$ - $Value :$ the amount of reserve tokens paid by users - $controlVariable :$ `terms.controlVariable` whose rise and fall is controlled by function `adjust()` - $\Delta block :$ the current `block.number` minus the `block.number` at the last call - $vestingTerm :$ the total number of blocks for bond issuance set by the `owner` - $totalDebt :$ record the total issuance of bonds - $totalDebt' :$ record the latest total issuance of bonds - $totalSupple :$ record the total iss
Open the reportThe OHM token contract does not grant the MINT role to the `Treasury` contract in its constructor or initialization phase. ```solidity=457 constructor( address _feeReceiver, uint _buyFeeRatio, string memory name_, string memory symbol_, address[] memory _coolingProtectors, uint _coolingPeriod ) ERC20Token(name_,symbol_) { feeReceiver = _feeReceiver; buyFeeRatio = _buyFeeRatio ; _setupRole(DEFAULT_ADMIN_ROLE,msg.sender); _setupRole(INTERN_SYSTEM,msg.sender); _setupRole(INTERN_SYSTEM,_feeReceiver); _setupRole(COOLING_PROTECTOR,_feeReceiver); for(uint i = 0 ;i< _coolingProtectors.length;i++) { _setupRole(COOLING_PROTECTOR,_coolingProtectors[i]); } coolingPeriod = _coolingPeriod; } ```
Open the reportThe linked contracts import and use OpenZeppelin's `Counters` contract. OpenZeppelin has deprecated the usage of the `Counters` contract: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/4233
Open the reportIt is expected that calls of the form `approve(spender, amount)` fail if the address in `spender` is the zero address. It is expected that calls of the form `transferFrom(from, dest, amount)` fail for transfers from or to the zero address. However, the `transferFrom` function in `sERC20` can transfer tokens from or to the zero address. It is expected that calls of the form `transfer(recipient, amount)` fail if the recipient address is the zero address.
Open the reportThe implementation of the `isContract` check can not cover all scenarios. The check can be bypassed if the call is from the constructor of a smart contract or when the contract is destroyed. Because, in that case, the codesize will also be zero. The "isContract" function in the OpenZeppelin "Address" library uses the same implementation, but comments mention that "it's unsafe to rely on the check and it can be bypassed". Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol
Open the reportThe default admin role has the ability to call the `setRatio()` function to adjust the `feeRatio` and `buyFeeRatio` of the ERC-20 token X, without a specified maximum limit. In extreme scenarios, these rates could be set as high as 100%, effectively meaning the receiver would not receive any tokens from buying or selling activities. Additionally, the default sell fee rate is set to 20%, which is unreasonably high. ```solidity=483 function setRatio(uint8 ratioType,uint256 ratio) external onlyDefaultAdmin { require(ratio <= PRECISION, "Exceeds precision"); if(ratioType ==0){ buyFeeRatio = ratio; } else { sellFeeRatio = ratio; } emit FeeRatioChanged(ratioType,ratio); } ```
Open the reportThe `permit` function performs the operation of deriving signer address from the signature values of `v`, `r` and `s`. The state variable `DOMAIN_SEPARATOR` that is used to calculate hash has a value of `chainid` that is derived only once in the constructor, which does not change after contract deployment. The issue arises in the event of fork when the cross-chain replay attacks can be executed. The attack scenario can be thought of as if a fork of Ethereum happens and two different networks have id of for example `1` and `9`. The `chainid` coded in `DOMAIN_SEPARATOR` will be the same on contracts residing in both of the forks. If the chainid `1` is stored in the contract then the `permit` transaction signed for chainid `1` will be executable on both of the forks.
Open the reportBy using the `incurDebt()` function in the `Treasury` contract, an approved debtor is allowed to borrow `_token` as long as they hold `sOHM`, with the maximum borrowing amount being equal to their `sOHM` balance. This process could lead to issues, as `sOHM` can be transferred to another approved debtor. For instance, if user A has 100 `sOHM` and borrows 100 `_token`, A can then transfer the `sOHM` to B, who is then also able to borrow 100 `_token`, and this pattern can continue indefinitely. Furthermore, a debtor can: 1. Use `Treasury.incurDebt()` to borrow `_token`. 2. Then use `Treasury.deposit()` to deposit these `_token` and receive `OHM` tokens. 3. Subsequently, use `Staking.stake()` to stake these `OHM` tokens and `Staking.claim()` to acquire `sOHM` tokens. 4. As the debtor's `sOHM` balance increases, they could repeat this cycle to accumulate more tokens, exacerbating the potential for loss.
Open the reportThe following description is adapted from OpenZeppelin's ECDSA file: [EIP-2](https://eips.ethereum.org/EIPS/eip-2) still allows signature malleability for `ecrecover()`. Appendix F in the [Ethereum Yellow paper](https://ethereum.github.io/yellowpaper/paper.pdf), defines the valid range for s in (311): `0 < s < secp256k1n ÷ 2 + 1` and for the recovery identifier (312): `v ∈ {0,1}`. This should not be confused with the input for `ecrecover()` where `v ∈ {27,28}`. (See [doc](https://ethereum.github.io/execution-specs/autoapi/ethereum/frontier/vm/precompiled_contracts/ecrecover/index.html)) However, these values can be obtained by taking `27 + "recovery identifier"`, so that they will also yield a unique signature and are often the `v` values returned from signatures. (For example [`web3.eth.accounts.sign()`](https://web3js.readthedocs.io/en/v1.7.5/web3-eth-accounts.html)) The signature malleability is possible within the Elliptic Curve cryptographic system. An Elliptic Curve is symmetric on the X-axis, meaning two points can exist with the same `X` value. In the `r`, `s` and `v` representation this permits us to carefully adjust `s` to produce a second valid signature for the same `r`, thus breaking the assumption that a signature cannot be replayed in what is known as a replay-attack. ```solidity address signer = ecrecover(_hash, v, r, s); require( signer != address(0) && signer == owner, "ZeroSwapPermit: Invalid signature" ); ```
Open the reportThe linked variables assigned in the constructor can be declared as `immutable`. Immutable state variables can be assigned during contract creation but will remain constant throughout the lifetime of a deployed contract. A big advantage of immutable variables is that reading them is significantly cheaper than reading from regular state variables since they will not be stored in storage.
Open the reportThe `_burnFrom()` function in the `ERC20` contract is public but with an underscore prefix, typically indicating an internal function. This implementation does not comply with the solidity best practices and we would like to confirm if this is the intended design.
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="nova-bank"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
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