RociFi’s Audit Results and Improvements
Jun 22
Do you even need an audit? Does it add any value? Does CompanyX audit provide any value compared to CompanyY’s? These questions are on any Solidity developer’s lips.
In this piece, we share the RociFi development team’s experience.
TL;DR: An audit is a good start but you need much more to increase the odds of a safe launch.
RociFi solves permissionless, under-collateralised lending in DeFi. To put it simply, our protocol allows you to borrow more money than you put as a collateral if your credit risk score is good enough. Our scoring engine checks your on-chain financial history and social reputation, then produces a score. No KYC is involved. As a lender, you can provide liquidity to the lending pools without lockup.
Protocol development started less than 10 months ago, but only now do we realize in retrospect how ambitious our product is. Just to name a few of the protocol’s core features
- Support for both under-collateralized and over-collateralized loans
- LTV and APR customizability per loan — in the same pool you can mix loans with the different LTVs and APRs
- Credit risk scoring support — protocol can use any numerical-based external credit scoring system
- Multiple loans can be collateralized by the same collateral type — you can deposit 1 WETH and make multiple loans against it, add and withdraw collateral any time (if the loan’s LTV permits)
- Flexible liquidations — by loan health check or maturity date (no instant liquidations)
- Flexible limit system for borrowing and depositing per user, per pool and protocol
- Rich roles and service discovery support
We wish we could have just forked Aave or Compound with a few tweaks and called it a day. But, in order to support the aforementioned rich features, we needed to develop a protocol from scratch.
After making the initial version, we involved auditors and our advisors to get honest feedback. We want to say a huge thanks to both Chainsulting and Certik teams for providing detailed reports and ideas for future improvements. The same goes for our advisors from the Lido.fi core tech team.
Our audit reports can be seen here: Chainsulting and Certik.
To tell the truth, most of the points raised by audits already were in the back of our minds. Still, it was super useful to get a confirmation for all the problematic or over-complicated parts of our design. This reinforced some of our ideas about further protocol architecture improvements.
A few key takeaways from our audits.
Liquidity Pools and price balancing functionality didn’t fit protocol requirements
Details: We used Uniswap Liquidity Pools (`IUniswapV2Router02` and `IUniswapV2Factory`) and our custom contract (`LiquidityFarm`) to allow anyone to deposit assets in the protocol and earn APY in form of farming rewards which paid in so-called “debt token”. The stability of the debt token is managed by the special method, `TraderInvestor.balance`. This method sells and buys debt tokens in the corresponding pool when the price fluctuates too much. `Trader.sol` contract is used to that end.
Problems:
- Uniswap pools could not be controlled.
- Debt token price could not be guaranteed.
- LiquidityFarm methods math could not be verified.
Fix: We removed Uniswap pools and AMM functionality from the protocol and created our pools with the simple and predictable debt token price logic which can be thoroughly tested.
Relying on Chainlink Oracle for critical business logic
Details: We used Chainlink oracles and Any API when confirming the loan. Namely, *Investor.borrowFulfill* method is called from the Oracle, which in turn calls *Investor.sendFunds*.
Problems:
- Oracle nodes could be attacked and thus initiate the undesired token transfer.
- Oracle nodes can be attacked and thus issue fake credit scores to issue loans with the high LTV.
- On top of the above mentioned challenges, we struggled a lot with building a stable testing environment. In the end, we had to set up our Chainlink node to support Any API calls.
Fix: We refactored away Chainlink Any API and simplified the complex and cumbersome process of pushing scores in the contract. Now Chainlink is used for the price feeds only.
Security and code instrumentation
Problems:
- Protocol contracts didn’t have an emergency switch.
- Only `ownable` was used for access control, which is very limiting.
- Protocol had very few events.
Fix: Now — the all-important functionality can be paused when needed, i.e. kill switch. We use rich roles management and service discovery provided by Openzeppelin’s access role system. Every meaningful change in the contract will fire an event, which allows for integrations with alerting and analytics software (Dune Analytics, Subgraph, OpenZeppelin Sentinels to name a few).
Code quality
Problems:
- Our test coverage was lower than 30%.
- Hundreds of compiler warnings were adding a lot of noise and degrading the developer experience.
- Lack of consistency in terms of code guidelines and formatting created lots of distractions for developers.
- Lack of code quality control led to constant regressions, longer development loops and overall team frustration.
Fix: Now — our test coverage is at 92%. Report. We have exactly 0 warnings. We have a code baseline in using Prettier and Solhint. We use Husky pre-commit hooks and Github actions to make sure new PR does not introduce regressions or diverge from the code style.
By all accounts, receiving an audit is a good metric. But, that’s only one piece of the puzzle. There are other pieces which are crucial to getting your project off the ground along with implementing the audit recommendations.
Testing-driven development
We cannot stress enough the importance of having the maximum possible test coverage of your contracts. Smart contracts engineering is not a very forgiving field. One mistake in your code can lead to a huge loss of liquidity for your customers. Not tested code should not be merged.
Hardhat is a great framework which became an industry standard. It allows you to run your tests in a rich variety of environments and provides lots of helpers and plugins.
Flexible testing environments
To properly test your Dapp, you need to deploy it to publicly accessed environments. For UI, we use Netlify branches, smart contracts are not that simple in this regard. Testnets help up to a point, but our experience with some of them was very painful. Your transactions might be stuck in the pool for hours for no reason, or block explorer may be suddenly inaccessible, and farming test ether is awkward. And what’s more important, no testnet is close enough to the target mainnet in terms of integrations, specific ERC-20 tokens and AMMs.
This is why we ended up spinning our fork node using Hardhat. And recently we switched to the Tenderly Forks, which also works quite well and allows for a rich debugging experience.
Tooling
Solidity development is still in its infancy and many developers are debugging their code using `console.log`. Luckily for us, we soon enough discovered Tenderly, which provides excellent debugging experience with breakpoints and expressions evaluation. We highly recommend any smart contract developer to integrate Tenderly in their environment.
Security and operational monitoring
Monitoring your contracts is essential. As an absolute minimum, you should have alerts for any non-sanctioned privileged transactions, big changes in liquidity and large value transactions, spikes in reverts and collateral price changes.
OZ Sentinels is a good start for basic alerting, and Forta Network bots provide a flexible and scalable framework for detecting on-chain events of any complexity. Alerts can be then filtered and routed to the pagerduty software of your choice.
Release and change management
Smart contracts development is no different from traditional software engineering and change management is an integral part of it. These principles may sound trivial, but following them increases the quality of your delivery.
- Any change in the code should go through PR, peer code review and testing.
- All deployments should be done via script and be immutable, meaning repeating script with the same input should bring the same results in terms of deployed contracts and their configuration
- All dependencies in the project should be carefully watched and revised once in a while
- Team should stick to semantic versioning or a similar system to mark significant changes in the interfaces for the external clients.
Smart contract development is a complex venture where audits service a critical, but ultimately, only one small piece of the puzzle. Having a robust testing and implementation framework, coupled with strong advisors to offer feedback at every turn significantly increases your chances of success (a shilling tweet from Elon Musk never hurts either).
This is the approach that RociFi has taken for its upcoming Mainnet launch on Polygon. Minus the tweet from Elon, although if interested, holla at us Elon!
After launch, we will implement a bug bounty program designed to further identify and mitigate threats to our protocol and stakeholders.
________
RociFi is bringing under-collateralized credit to Web3