Blog

Vest in Peace: Freezing Cosmos account funds through invalid vesting periods

Written by Yaar Hahn & Dima Kogan | Jul 13, 2023 12:33:28 AM

Summary

This blog post describes a vulnerability we recently discovered in the Cosmos SDK, which impacts more than a dozen Cosmos app chains, including Cronos, Kava, and Osmosis. The vulnerability allows an attacker to front-run any account-creating deposit with a malicious transaction, which permanently locks the victim’s deposited funds. We have privately disclosed the vulnerability to the Cosmos SDK team, who codenamed it Barberry and quickly released a fix.

The vulnerability stems from an incomplete validation of messages used to create certain types of Vesting Accounts. Specifically, it allows an attacker to poison any previously unused address by initializing it as a vesting account an invalid vesting schedule. The poisoned account still admits deposits (which is what enables the front-running attack described above), yet it prevents any subsequent withdrawals from the account.

Vesting accounts

Before diving into the vulnerability, we give some background on how vesting accounts work. A vesting account is a type of account in the Cosmos SDK, whose coins are initially locked and are then released according to a predefined vesting schedule. In a periodic vesting account, the vesting schedule consists of a sequence of periods, each of which specifies an amount and length. A clawback vesting account is another variant of a periodic vesting account, which enforces an additional lock period, during which vested funds can be clawed back.

When creating a new periodic vesting account, the creator sets the vesting periods for the account and transfers the total amount in the vesting periods, referred to as OriginalVesting, into the new account. At the end of each vesting period, the coins associated with that period are unlocked and made available for transfers to the owner of the account (the holder of the private key corresponding to the public key, if exists).

The owner of the vesting account can use the account in parallel to the vesting periods — additional funds might be transferred into the account, and any unlocked assets can be transferred out. The only limitation is that any unvested coins are kept locked. To enforce this, any time the owner of the vesting account tries to send coins from the account, the account enforces that the outgoing amount is no greater than the amount of spendable coins, which is the total balance in the account minus the amount that is still locked (i.e., has not vested).

Vulnerability

Both periodic and clawback vesting accounts contain a bug in their account creation code. They fail to fully validate the vesting periods, which are given to them as input by the creator of the account, as part of the transaction that creates the account. The code fails to validate that the amount in each vesting period is positive, which allows an attacker to create a vesting account with negative vesting amounts in some of its periods. For example, consider the following code, taken from v0.46.12 of the Cosmos SDK.

 

Note that the code validates that each of the vesting periods has a positive length, but fails to validate that it has a positive amount.

Variants

There are several instances of the vulnerable code across the Cosmos ecosystem. They come in two main flavors.

The first flavor of the vulnerability is in Cosmos SDK’s PeriodicVestingAccount. In versions v0.46 and v0.47 of the Cosmos SDK, the bug is in the func (msg MsgCreatePeriodicVestingAccount) ValidateBasic() error function, which is normally used during the creation of the vesting account. In more recent versions, the bug is in the CreatePeriodicVestingAccount function. (Versions of the SDK earlier than v0.46 also contain the bug, but are not exploitable, due to the fact that in those older versions, PeriodicVestingAccounts could only be created at Genesis.) Any chain that uses a vulnerable version of the SDK and enables the vesting module is vulnerable. Vulnerable chains of the first variant include: Cronos, Iris, Kava, Kyve, Lum, Provenance, Quicksilver, Regen, Stride, Umee.

The second flavor affects Agoric, Evmos, and Osmosis, who all use an account type called ClawbackVestingAccount, which is a modification of the SDK’s PeriodicVestingAccount, and contains a similar vulnerability in the func (msg MsgCreateClawbackVestingAccount) ValidateBasic() error function.

Exploit

The main idea of the exploit is that an attacker can initialize a victim’s account as a malicious vesting account, which allows deposits but does not allow withdrawals. When the user then deposits funds into their account, those funds are locked forever, and the user is not able to withdraw them.

Poisoning an account

We first explain how an attacker can “poison” an account, such that it no longer allows withdrawals. As an example, consider a malicious vesting account created with the periods:

Creating this account requires funding it with 2stake - 1stake = 1stake, so very limited funds are needed for the attack. The malicious account unlocks 2stake coins on the next block following its creation. However, any attempt to withdraw funds from the account will fail for the following reason. Recall that on any withdrawal attempt, the code needs to check the withdrawal amount against the available balance in the account (see this check in the Bank module). This check requires computing the amount of coins in the account that have not yet vested. The vesting module computes this amount by subtracting the amount of coins that have already vested from the original vesting amount (see here):

For the poisoned vesting account, due to the negative amount in the second vesting period, the OriginalVesting amount is smaller than the VestedCoins amount. This causes an invalid Coins.Sub operation, triggering a panic and preventing any withdrawal (see here):

Preying on a victim

Recall that a vesting account can be created by a user at any address corresponding to a public key of any other user, as long as no account has been created yet at this address. The attack works by preying on new accounts, front-running their creation, and then poisoning them.

Specifically, the attacker first waits for a new account-generating transaction to arrive in the mempool. The attacker thens send a malicious transaction that front-runs the account creation, and creating a poisoned vesting account at the same address. This race is especially feasible in an IBC transfer that creates a new account, as there’s a big window of time between the transaction on the origin chain and the transaction on the destination chain. Any amount transferred into the account as part of the original account-generating transaction will be deposited in the account but could never be withdrawn again due the account being poisoned.

Fixing the vulnerability

Fixing the code is straightforward: the code should validate that the amounts in all of the vesting periods are positive.

Following our report (on May 27), the Cosmos SDK team published a security advisory (on June 7) and released fixes for v0.46 and v0.47 (on June 8). They also worked with the different chains in the ecosystem to ensure they upgrade to the latest version.

We thank Marko Baricevic from the Cosmos SDK team and Dev Ojha from the Osmosis team for their prompt response in handling the vulnerability disclosure.

Proof of Concept

Our proof-of-concept code works as follow:

  1. Start the chain.
  2. Fund the attacker’s account with tokens.
  3. Attacker sends a malicious transaction and poisons the victim's new account.
  4. Victim deposits additional funds into their new account.
  5. Victim tries to withdraw their (unlocked) funds from their account and fails.

See the code of our proof-of-concept for the Cosmos SDK variant and the Agoric/Evmos/Osmosis variant.