Brief Update

On Sunday, June 21st, security researcher @samczsun privately disclosed two vulnerabilities in the currently deployed contracts and lender agents.

Both vulnerabilities would've allowed a malicious borrower to unlock part/ all of their  BTC collateral without repaying their loan in specific circumstances.

Neither of these vulnerabilities were exploited by any users, and there were no funds impacted on the platform. Additionally we have disabled the ability for any borrower or lender to participate in new loans until we launch v2.

You might wonder how we paused the contracts when we don't have any admin key to do so in current protocol. In truth, we didn't actually pause the contracts (and don't have the ability to). In Atomic Loans, lenders run bots, and borrowers make requests to these bots for a loan. When developing the protocol at this early stage, we opted to enable upgradeability with these bots to ensure a vulnerability could be quickly fixed (a feature which current lenders opted into that allows lender agents to be updated after an hour of a new release on Github). In this case, pausing meant updating the loan request part of the bots to disallow loan requests.

Although we had two audits done on the smart contracts, as well as an internal audit done by the team, it showed us it is still possible for these types of issues to slip through the cracks.

On Monday, June 23rd, we pushed an update to lender agents, effectively disallowing new loan requests

As a result, no funds were lost and all existing loans are safe. We will continue to support users with existing loans until the end of their loans.

We’ve seen a tremendous amount of activity and feedback since our initial public beta launch in April. With this feedback in hand, we’ve already begun work on an improved v2 version of the protocol. Given that v2 is already in progress and our desire to err on the side of caution when it comes to user funds, we will continue to disallow new loan requests until the launch of v2.

A big thank you to @samczsun for helping to make the decentralized finance ecosystem safer.

How could these issues have been exploited?

A malicious borrower could’ve unlocked their BTC collateral without repaying their loan by front-running a loan cancellation transaction from the lender after the lender secret has already been revealed in the mempool.

Lender funds could have been impacted if the vulnerability had been exploited and the lender is unable to pay a high enough gas fee to ensure the loan cancellation succeeded.

What @samczsun found?

Front-running collateral unlocking attack

In cross-chain systems such as atomic swaps, the security of the swap is dependent on the order and timing in which transactions occur.

In the case of Atomic Loans, ensuring that collateral is always backing a particular loan is dependent on the secret required for the borrower to unlock the collateral not being revealed until repayment or cancellation occurs.

Generally, the lender only cancels the loan after a certain condition is met.

If the Borrower doesn't lock collateral, then the Lender won't broadcast the approve tx allowing the Borrower to withdraw.

However if the Borrower does lock collateral, the Lender will give the Borrower up to 24 hours to withdraw the loan. If the Borrower does not do so, the lender will cancel the loan.

When a Lender calls the cancel function, they reveal a secret in the process, allowing the Borrower to unlock their collateral.

However, this tx is not instantly confirmed on Ethereum, which means that the secret necessary to unlock the BTC collateral exists in the Ethereum mempool for some time, before changing the state of the loan.

This is fine if the loan has not already been approved, since the Borrower cannot withdraw anyway.

If an attacker was able to front-run the cancel tx after the loan was approved with the withdraw function, they could take the secret from the cancel tx in the mempool and also withdraw USDC/DAI for the loan. This would allow them to withdraw the principal of the loan, and also unlock their collateral without repaying.

Solution to the issue

This vulnerability could be easily fixed by adding a withdrawExpiration to the withdraw function in the Loans contract.

Here is the current withdraw function

function withdraw(bytes32 loan, bytes32 secretA1) external {
  require(!off(loan), "Loans.withdraw: Loan cannot be inactive");
  require(bools[loan].funded == true, "Loans.withdraw: Loan must be funded");
  require(bools[loan].approved == true, "Loans.withdraw: Loan must be approved");
  require(bools[loan].withdrawn == false, "Loans.withdraw: Loan principal has already been withdrawn");
  require(sha256(abi.encodePacked(secretA1)) == secretHashes[loan].secretHashA1, "Loans.withdraw: Secret does not match");
  bools[loan].withdrawn = true;
  require(token.transfer(loans[loan].borrower, principal(loan)), "Loans.withdraw: Failed to transfer tokens");

  secretHashes[loan].withdrawSecret = secretA1;
  if (address(col.onDemandSpv()) != address(0)) {col.requestSpv(loan);}

  emit Withdraw(loan, secretA1);
}

By adding a withdrawExpiration requiring the Borrower to withdraw in a certain timeframe, it would ensure that a Borrower couldn't front-run.

require(now <= withdrawExpiration(loan), "Loans.withdraw: Loan is past the withdraw deadline");

Other vulnerability found by @samczsun

RBF collateral locking attack

A couple of months ago, we noticed an attack that could occur to lender agents, by essentially tricking them that the necessary Bitcoin was locked as collateral. Although we fixed this issue several months ago, there was a secondary instance of this check that went unnoticed in the API of lender agents which was forgotten about, that would allow it to be exploited.

This could be achieved because the agent was only checking if the first utxo was confirmed for locking the collateral.

const refundableBalance = await loan.collateralClient().chain.getBalance([collateralRefundableP2SHAddress])
const seizableBalance = await loan.collateralClient().chain.getBalance([collateralSeizableP2SHAddress])

const refundableUnspent = await loan.collateralClient().getMethod('getUnspentTransactions')([collateralRefundableP2SHAddress])
const seizableUnspent = await loan.collateralClient().getMethod('getUnspentTransactions')([collateralSeizableP2SHAddress])

const collateralRequirementsMet = (refundableBalance.toNumber() >= refundableCollateralAmount && seizableBalance.toNumber() >= seizableCollateralAmount)
const refundableConfirmationRequirementsMet = refundableUnspent.length === 0 ? false : refundableUnspent[0].confirmations > 0
const seizableConfirmationRequirementsMet = seizableUnspent.length === 0 ? false : seizableUnspent[0].confirmations > 0

As you can see, it only checks the first the first utxo to see if it is confirmed:

refundableUnspent[0].confirmations

A potential attacker looking to take out a 5k loan, and was aiming to lock 1 BTC as collateral (say 1 BTC = 10k) could lock 0.1 BTC first, wait for it to confirm, and then lock the remaining 0.9 BTC. The lender agent would mark the collateral as locked and approve the loan.

The attacker could then replace-by-fee (RBF) the utxo with 0.9 BTC and send it to an address they own.

This would allow the attacker to exit with 5k loan, and 0.9 BTC, only locking 0.1 BTC in the process.

How we reacted?

  • After first hearing about the RBF collateral locking attack vulnerability we created a PR for lender agents to ensure to fix the issue. We then pushed an update to lender agents (a feature which current lenders opted into that allows lender agents to be updated after an hour of a new release on Github). https://github.com/AtomicLoans/agent/pull/179
  • Once we learned of the front-running vulnerability, we made the decision that the system could not allow for future loan requests without redeployment. We updated the endpoints for requesting a new loan in current agents to take no action at all, and simply return a 401. This essentially pauses the current system. We pushed an update to the agents for this as well. https://github.com/AtomicLoans/agent/pull/181

What does this mean for me?

  • If you already have a loan, or you are currently a lender, your funds are safe. The main vulnerability was associated with the origination of new loans.
  • However if you wish to take out a new loan, you unfortunately cannot do so at this stage as we have paused new loan requests.
  • If you are an existing customer, we will continue to support you. We will continue to support users with existing loans until the end of their loans.

If you are a borrower:

  • Your Bitcoin is safe. All the vulnerabilities disclosed were related to lender funds. As long as you repay your loan before the loan expiration you will be able to unlock your BTC.

If you are a lender:

  • Your stablecoins are now safe. However, there will be no new loan requests, so eventually once loans are repaid, you'll basically just be making compound finance interest rate. Feel free to withdraw whenever you wish.

What we learned and next steps

  • We are grateful to @samczsun for his very dilligent work, and even though no funds were lost, we recognize this vulnerability will lead to concern amongst our community members, and for that, we deeply apologize.
  • We have notified the issues to both our previous auditors, ConsenSys Dilligence and Quantstamp for additional feedback. We want to do our part in helping the auditing community understand how we can better identify these types of vulnerabilities in cross-chain systems moving forward.
  • For now, we are disallowing new loan requests (although technically lenders can run their agents however they wish, we don't recommend allowing new loan requests).
  • We want to thank our community for all of their feedback since our initial public beta launch in April. An improved v2 version of the protocol was already in the works after hearing all this feedback, with the expectation that we would eventually be deprecating v1 in the near future. Given this, and more importantly, our desire to err on the side of caution when it comes to user funds, we will continue to disallow new loan requests until the launch of v2.
  • At the time of pausing, the Atomic Loans v1 beta was at $255,173 in stablecoin supplied, $90,290 in stablecoin borrowed, and ~23.9 BTC locked (total value locked: $485706).  We also reached a total of $202,333 in total loans originated since launch.

Last note

As we mentioned earlier, seeing as though these issues arose even after multiple external audits on the smart contracts as well as an internal audit done by the team, it showed us it is still possible for these types of issues to go unnoticed. That being said, we must do better. Part of this will mean getting more eyes on the protocol earlier on, as well as more intensive audits in the early stages.

Additionally, ahead of V2 launch, we are planning on organizing a white-hat hacker event, in addition to multiple audits and implementing a bug bounty program.

If you have any further questions and feedback, feel free to reach out to us on Telegram or by email at [email protected]