Our Smart Contract Security Principles
- Vyper >>>>
- Foundry >>>>
- Testing methods
- game theory role playing
- unit testing
- integration testing
- fuzzing
- onchain fork testing
- TODO: formal verification
- TODO: agent based modeling simulations
Testing Debt DAO Contracts
# Setup
$ git clone https://github.com/debtdao/Line-of-Credit.git lines
$ cd lines
$ foundryup
$ forge install
# Run all tests
$ forge test -vvv
# Test individual test files
$ forge test —match-path <filepath>
# Test individual tests
$ forge test —match-test <testname>
# Check test coverage
$ forge coverage
Config
Before running tests, make sure the foundry.toml
file is correctly configured. Make sure it includes the following:
[profile.default]
src = 'contracts'
test = 'test'
script = 'scripts'
out = 'out'
libs = [
]
remappings = [
"forge-std/=lib/forge-std/src/",
"ds-test/=lib/forge-std/lib/ds-test/src/",
"chainlink/=lib/chainlink/contracts/src/v0.8/",
"openzeppelin/=lib/openzeppelin-contracts/contracts/"
]
libraries = []
Check the the .env
file includes the following environment variables:
FOUNDRY_PROFILE=""
MAINNET_ETHERSCAN_API_KEY= <YOUR_KEY_HERE>
DEPLOYER_MAINNET_PRIVATE_KEY= <YOUR_KEY_HERE>
MAINNET_RPC_URL= <YOUR_RPC_URL_HERE>
GOERLI_RPC_URL= <YOUR_GOERLI_RPC_URL_HERE>
GOERLI_PRIVATE_KEY= <YOUR_GOERLI_PRIVATE_KEY_HERE>
LOCAL_RPC_URL='http://localhost:8545'
LOCAL_PRIVATE_KEY= <LOCAL_PRIVATE_KEY_HERE>
If FOUNDRY_PROFILE
is not set to to goerli
or mainnet
the tests will fail trying to find deployed libraries. Always set FOUNDRY_PROFILE
to ""
to ensure correct testing. If MAINNET_RPC_URL
is left empty, test files that need a mainnet fork (such as the Dex.t.sol
file, will fail.
Testnet
The simplest way to interact with Debt DAO in a test environment is to connect your wallet and use the app with fauceted funds on a test network. All test markets can be accessed from the app or from cloning/forking the Debt DAO frontend.
To interact with test markets make sure you toggle the chosen network to Goerli. The testnet option is available on app in top right drop-down-menu.
You can grab Goerli ETH here: Goerli Faucet
Certain aspects of the app will appear broken. Many of the Lines of Credit on Goerli use fake tokens that have no. chainlink oracle and thus, no dollar value. Therefore, all the fields that use dollar amount will appear 0.
Unit Testing
We have unit tests written for each public function available in our smart contracts. These tests are divided into files that cluster different features or logical components in our system. Tests are designed to make sure functions perform the expected action under the correct conditions and fail under incorrect conditions. We do invariant and fuzz testing as much as possible.
Each test files sets up the scenario in the setup()
function. This usually creates a Line of Credit, mints tokens to borrowers and lenders as well as instantiating any peripheral contracts that will be interacted with (Oracles, InterestRate Contract).
We use the Foundry Framework for testing, which allows for Solidity based testing. The benefit of this is that the tests more accurately describe the intended functionality of our smart contracts, and allow for more accurate debugging using the -vvv flag. Additionally, it speeds up development by simplifying the steps needed to create tests for new functionality. You can find more detailed documentation on Foundry here: Foundry Docs.
Maintenance is necessary any time read or write functionally is added, or if a function return is altered.
Integration Testing
Several test files test our individual modules integration with each other. Spigoted Line, Escrowed Line, and Secured Line test that the individual Escrow and Spigot modules integrate with our Line of Credit module properly and that the combined module logic operates as expected.
Additionally, we have test files that evaluate our products compatibility with external services, specifically the 0x API , 0x Exchange, and Chainlink oracles.
Onchain Fork Testing
We test all external integration tests on mainnet forks. Additionally we use mainnet forks to test new features and fixes or develop new products on top of our previously deployed contracts.
Primary mainnet tests:
- 0x API offchain API responses execute onchain at the right price
- Chainlink oracle failures cause appropriate error paths in our wrapper contract
- Pool contract integration with underlying Line contracts they invest in
- Pool contract integration with Lines they borrow from
- Pool contract integration with Spigots they attach
Agent-Based Modeling
As we continue to expand our marketplace and integrate with other platforms, it becomes increasingly difficult to test all scenarios. Simple rules can lead to complex behavior and Interoperability leads to increased complexity. Therefore, it becomes necessary to constantly test our products within simulated environments in order to catch potential attack vectors, non-optimal parameter settings and other unforeseen situations.
The goal is to create baseline behaviors with 0 intelligence and optimizing agents and then use Reinforcement Learning Agents to push the boundaries of our system.
Our ABM framework with be written in Python and will leverage XXX and XXX to simulate our Line of Credit and Pools products.
- Types of Agents
- 0 intelligence
- Hard coded, no optimization: buy and sell randomly, 50%. Not changing behavior.
- Optimizing Agent
- They understand how markets work and how other agents work. They do not change their behavior.
- Reinforcement Learning Agents
- Look at their actions and change their behavior
- Try to explore the whole market
Known Edge Cases & Security Risks
In this section, we explain some edge cases and risk situations, whether they are managed and covered automatically, and what the procedure is case by case.
Attack vectors mainly focus around the risk to Lenders' deposits, there are also risks to consider for Borrowers too.
Bounties are available for new stuff so keep digging!
Bad Actor Claiming Revenue Tokens from a Spigot
related to claimRevenue()
Attack Vector
A Revenue Contract could be called by a bad actor via claimRevenue() with the intention of siphoning off Revenue Tokens.
Background Information
claimFunction is the function signature to call a Revenue Contract and claim Revenue Tokens
claimRevenue() claims Revenue Tokens and leaves them in escrow (i.e. unlike claimAndTrade() and claimAndRepay() it doesn’t convert Revenue Tokens to Credit Tokens to be made available for Lender withdrawal or actually repaid to Lenders respectively).
Mitigation Strategies
The contracts are written such that claimFunction is restricted to being only capable of calling a Revenue Contract with the sole purpose of escrowing Revenue Tokens.
claimRevenue() can only call the predetermined claimFunction as set by the Spigot Owner
claimFunction can’t be called in operate(), the Spigot function that allows a Borrower to call the whitelisted functions on its Revenue Contracts and carry on its business as usual activities.
The Borrower should verify that claimFunction does not modify any other state on the underlying Revenue Contract. A further mitigation strategy implemented is limit access to the Arbiter for claimAndTrade() and claimAndRepay().
Negative Impact of Whitelisted functions on the Spigot
related to updateWhitelist(), updateWhitelistedFunction() and operate()
Attack Vector
A bad actor could propose a whitelisted function that affects the normal functioning of the Spigot
Background Information
Whitelisted functions are those allowed to be performed on a Revenue contract so that the Operator (Borrower) can still use the contract whilst providing Revenue Tokens to repay debt.
Mitigation Strategies
Only whitelisted functions can be called by an Operator on the Revenue Contract.
operate() can only work on Revenue Contracts authorised by the Spigot Owner
The Spigot Owner has verified the function logic of the whitelisted function and ensured that they poses no risk to the Spigot’s functionality
Upgradeable Revenue Contract
Attack Vector
A Borrower could upgrade the Revenue Contract securing the Line of Credit.
They could change the underlying logic of the already whitelisted functions.
They could create new functions with the same function signatures.
Either of these can cause the Spigot to lose control of the underlying Revenue Contract(s) and to not be able to claim Revenue Tokens.
Mitigation Strategies
Spigot Owners should only consider immutable smart contracts as candidates to be Revenue Contracts
Attack on Revenue Contract
Attack Vector
A Spigot could be deployed that has been approved for transferring Revenue Tokens to a malicious contract
Mitigation Strategies
Although this is mitigated by only letting the Owner add Revenue Contracts to a Spigot it could still be susceptible to social engineering.
Intentional Default by Borrower
Attack Vector
A Borrower could try to take out a loan partly secured by a Revenue Contract that they intend to deprecate or which they know won't make enough revenue to repay the loan with the agreed revenue split.
Mitigation Strategies
To ensure repayment to a Borrower’s full ability, the Spigot will automatically switch to escrowing 100% of Revenue Tokens if the loan is past due or if the spot value of any collateral becomes too low.
It is also theoretically possible to repossess a Borrower's entire protocol using contracts controlled by the Spigot and sell it off to an investment DAO or related protocol DAO to repay Lenders.
Malicious Spigot Owner
Attack Vector
A Spigot Owner could be configured to retain ownership of Revenue Contracts even though all debt has been repaid
Mitigation Strategies
Before giving control of a Revenue Contract to a Spigot, a Borrower should ensure that the Owner is a smart contract with the proper functions in place to later return ownership as and when appropriate.
The Owner should not be an EOA and if it is a smart contract that is not a Debt DAO Line of Credit contract then a Borrower should ensure it is verified on etherscan and has smart contract audits specifically related to those functions related to a Spigot integration.
Borrower Trading Revenue Tokens for a Fake Credit Token
Attack Vector
Borrower creates a fake credit token and creates an LP pool with the fake credit token and a second token in which they earn revenue in (use ETH for simplicity). Pool has really high ETH price so they don’t need a lot of ETH to initiate an attack.
When calling claimAndRepay(), they trade the Revenue Tokens (ETH) into the fake credit token that they created allowing them to capture the value instead of Lenders being repaid.
Mitigation strategies
The Credit Tokens into which Revenue Tokens are converted are selected automatically according to the first position in the repayment queue. This way the acquired Credit Tokens will always be those which a Lender has deposited.
0x (the DEX we’re using) generally doesn’t support illiquid and/or unknown tokens
0x only allows 1 <>1 token trading so it can’t trade a tiny amount of the Revenue tokens into Credit Token and the rest to the fake token. It must trade all Revenue tokens into Credit Tokens A further mitigation strategy implemented is limit access to the Arbiter for claimAndTrade() and claimAndRepay().
Abuse of Split of Revenue Tokens from a Spigot
related to updateOwnerSplit() and mentioned in the Halborn audit Aug 2022 (HAL-10)
Attack Vector
In a Spigoted Line of Credit, the updateOwnerSplit() function could be abused by the Borrower or the Lender.
If the ownerSplit parameter is set below the defaultRevenueSplit parameter, the Lender could call it to increase the split percentage which would lead to more Revenue Tokens being used to pay off the debt more quickly.
If the ownerSplit parameter is set above the defaultRevenueSplit parameter, the Borrower could call it to decrease the split percentage. This would lead to less revenue being used to pay off the debt, and more revenue would return to the Borrower’s Treasury.
No authorization check is implemented for who can call this function.
Mitigation Strategies
Borrower and Lender have agreed to the defaultRevenueSplit terms so there's not really “abuse”
The terms are transparent and deterministic so can't really be exploited, just favorable for certain stakeholders in certain situations
It makes the default revenue split a schilling point for all revenue splits (we can customize per contract if we wanted to)
Borrower can still claim Revenue Tokens from a Spigot after failing to repay by the end of the term
related to claimRevenue() and mentioned in the Halborn audit Aug 2022 (HAL-19)
Background Information
If a Line of Credit hasn’t been fully repaid by the due date, the healthcheck () function must be called explicitly to set the status to liquidatable. The Arbiter can then act to ensure that 100% of Revenue Tokens are escrowed until the loan can be fully repaid.
Attack Vector
A loan’s liquidatable status is not automatically propagated to the Spigot if a Line hasn’t been fully repaid by its expiry date and healthcheck() has been run.
claimRevenue() function has no authorization implemented.
As a result, the borrower can front-run the Arbiter reset of the updateOwnerSplit() function with claimRevenue() to obtain one more revenue share from spigot.
Mitigation Strategies
All Borrowers and Lenders are incentivized to call claimRevenue() as frequently as gas allows because Borrows need cashflow and Lenders want to escrow as much collateral as possible.
Although the risk is not zero, we expect the surface area to be negligible compared to the size of the loans and interest payments.
Malicious Arbiter could transfer Spigot Ownership to a Borrower before a loan has been repaid
Mentioned in the Halborn audit Aug 2022 (HAL-22)
Background Information
When setting up a Secured Line of Credit, it’s assumed that a Borrower adds a Spigot with a Revenue Contract and transfers the ownership of that Spigot to the Line of Credit, acting as new Owner for the benefit of Lenders.
The Arbiter can call updateWhitelist() to allow or disallow execution of the transfer ownership function for the Operator (Borrower) for the Revenue Contract.
Attack Vector
A malicious Arbiter could whitelist the transfer ownership function for the Borrower.
The Borrower could then transfer the ownership from the Line back to itself using the _operate() function.
The Borrower can already have drawn down funds before this attack, leaving Lenders with no recourse.
Mitigation Strategies
Whilst an Arbiter is supposed to be a 3rd party negotiator between all Lenders and a Borrower, based on its roles in a Line of Credit the Arbiter is more like an advocate or an agent for Lenders.
Lenders should select trusted arbiters as that is the only trusted part of our system currently but we plan on automating/decentralising them later.
Borrower can prevent Lender from withdrawing (and can minimize drawn interest accruing)
Mentioned in the Halborn audit Aug 2022 (HAL-24)
Attack Vector
When a Lender attempts to withdraw(), a Borrower could front-run it with the borrow() function, and immediately call the repay() function to repay what it had drawn down.
No drawn interest would be applied and all funds would still be available to borrow.
The borrower could repeat that until the end of the term at which point the Borrower wouldn't be able to borrow again or would be liquidated if they didn't fully repay
Mitigation Strategies
Whilst this is technically possible, a flashbot transaction sent by the Lender ought still be able to withdraw the funds
Enabling token collateral - internal transaction error
A Borrower can post any ERC-20 or ERC-4626 token as collateral, provided that the token has been whitelisted (enabled) by the Arbiter. If however enableCollateral() is called for a token that is not ERC-4626 compliant then an internal transaction error will be returned (an example is provided below)
Whilst ERC-4626 tokens can be enabled, only the underlying collateral is used for pricing. ERC-20 and ERC-4626 contracts must be verified for malicious code / exploits before enabling.
Lender can deposit after Line expiry
A Borrower can no longer draw down after a Line has expired.
If however there are no outstanding drawdowns at expiry and therefore the Line is not in default, it's still possible for a Lender to deposit funds to the Line.
This creates a 'technical default' which causes the Line to be liquidatable. In this case, a Lender simply has to withdraw the deposited funds.
Onchain Monitoring
We use OpenZeppelin Defender to run cron jobs and react to onchain events. This includes automated daily reporting sent to our discord, initiating borrower defaults (but not liquidations!), and other simple operational tasks.
Leveraging our Agent Based Modeling simulation framework, we have created edge case scenarios that we want specifically monitor. As our simulation framework advances, the number of
Platform Level Risks
- integrations
- smart contract risks
- trust assumptions
- other assumptions (e.g. spigot isnt hacked == trustless repayment)
Our Auditors
All previous audits
Other Resources
🚨 🚧 🚧 🚧 🚨 Under Construction 🚨 🚧 🚧 🚧 🚨