Edge Cases and Risk Situations
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!
related to claimRevenue()
A Revenue Contract could be called by a bad actor via claimRevenue() with the intention of siphoning off Revenue Tokens.
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).
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().
related to updateWhitelist(), updateWhitelistedFunction() and operate()
A bad actor could propose a whitelisted function that affects the normal functioning of the Spigot
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.
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
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.
Spigot Owners should only consider immutable smart contracts as candidates to be Revenue Contracts
A Spigot could be deployed that has been approved for transferring Revenue Tokens to a malicious contract
Although this is mitigated by only letting the Owner add Revenue Contracts to a Spigot it could still be susceptible to social engineering.
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.
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.
A Spigot Owner could be configured to retain ownership of Revenue Contracts even though all debt has been repaid
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 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.
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().
related to updateOwnerSplit() and mentioned in the Halborn audit Aug 2022 (HAL-10)
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.
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)
related to claimRevenue() and mentioned in the Halborn audit Aug 2022 (HAL-19)
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.
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.
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.
Mentioned in the Halborn audit Aug 2022 (HAL-22)
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.
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.
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.
Mentioned in the Halborn audit Aug 2022 (HAL-24)
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
Whilst this is technically possible, a flashbot transaction sent by the Lender ought still be able to withdraw the funds
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.
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.