The story of the curious rent thief

OtterSec OtterSec #solana#report

A tale of pickpockets preying on the Solana ecosystem. Read our investigation into the persistent theft of rent from uninitialized accounts. This is the story of the Solend rent thief.

The story of the curious rent thief

Introduction

Recently, there’s been a rent thief. This bot steals money from uninitialized accounts across the Solana ecosystem, claiming and profiting from the rent. The Solend team noticed the bot when it attempted an attack on the new permissionless pools that are being developed. Let’s dig into how rent thieving works by doing a case study on an attack on one of the permissionless pools.

Note

To be clear, funds stored in the main Solend protocol are completely unaffected.

Background

To understand how this exploit works, we first have to understand a bit about how rent works in Solana.

Since accounts can store data that every validator needs to download, Solana charges a certain amount of rent based on the amount of data. However, accounts that have enough for two years of rent payments are considered rent-exempt as long as their balance never drops below the threshold. Fortunately, rent is very cheap, so it’s not hard to make an account rent-exempt.

As such, when creating new accounts, most programs will need to transfer some SOL into the new account to make it rent-exempt.

The exploit

New reserves (also known as assets) are added to a Solend pool by calling the init_reserve function, which creates six new accounts to store data about the reserve:

  1. Reserve detail, which stores information about the reserve, e.g. liquidity mint, mint decimals, oracles, configs, etc.
  2. Reserve liquidity token account, which holds deposited tokens.
  3. Fee receiver token account, which will receive origination fees on borrows.
  4. Reserve collateral mint account, the deposit receipt token, also known as cTokens.
  5. Reserve collateral token account, which holds users’ collateral tokens.
  6. Creator collateral token account, the creator’s cToken account.

Account creation and initialization are usually done within the same transaction. However, due to Solana’s transaction size limit of 1232 bytes, the creation and initialization of these six accounts had to be separated into two transactions: creation and initialization. Here’s what a call to init_reserve is supposed to look like:

Diagram of the intended two-transaction init_reserve flow: accounts are created and funded in the first transaction, then initialized in the second

Notice anything amiss? In between the two transactions, the account has rent money but no owner. This is where the rent thief comes in to snatch the account, along with its rent:

Diagram of the attack: the rent thief claims the freshly funded account in between the creation and initialization transactions

Since there was a roughly 40-second (50-slot) window in between the two transactions, such an attack was very consistent.

Fortunately, rent is relatively cheap, so the entire attack only extracts about 0.0082 SOL every iteration (four token accounts each worth around 0.002 SOL), which is around 28 cents at the time of writing this article.

Despite this low cost, this is pretty annoying…

Example

Let’s take a look at a real attack.

Transaction 1:

Explorer view of transaction 1, where the developer creates several accounts and funds each with rent-exempt SOL

(…more accounts truncated)

The developer creates a couple of accounts and transfers enough SOL for them to be rent-exempt. This took place in slot 136,580,113.

Attacker’s transaction:

Explorer view of the attacker’s transaction taking ownership of the newly created accounts

(…more accounts truncated)

As detailed before, the attacker takes ownership of the newly created accounts. This took place in slot 136,580,154, which is 41 slots (29 seconds) after the initial transaction.

Transaction 2:

Explorer view of transaction 2, where the developer’s initialization attempt fails

Program log showing the “account or token already in use” error

The developer attempts to take ownership of the account, but it fails with the error “account or token already in use” since the attacker took ownership of it. This took place in slot 136,580,167, which is 13 slots (9 seconds) after the attacker’s transaction. In total, that’s a 54-slot gap (38 seconds) between the two Solend transactions.

Attacker’s transaction:

Explorer view of the attacker’s final transaction closing the accounts and collecting the rent

(…more accounts truncated)

Now that the attack is over, the attacker closes the accounts, transferring the rent money to themselves. The total money stolen during this attack was 0.00815212 SOL.

Impact

Rent-thieving attacks don’t steal much money.

They can only make a small profit very infrequently as Solana rent is cheap and there are only a handful of large services that separate account creation and initialization. In addition, this strategy doesn’t scale well, since such non-atomic account creation is relatively infrequent.

However, it’s still obnoxious even if the monetary impact is minimal. Transactions will fail and need to be remade, impacting usability.

Solution

As a temporary stopgap, Solend refactored their codebase to lower the 40-second delay between transactions to around 15 seconds (20 slots), making an attack much more difficult and inconsistent.

As a more permanent solution, Solend implemented an onchain program which handles account creation, allowing them to fit all the relevant instructions into one transaction.

Subscribe to the blog

New posts from the OtterSec team, straight to your inbox. One email per post, unsubscribe any time.