Light ModeLight
Light ModeDark

One Bug Per Day

One H/M every day from top Wardens

Checkmark

Join over 1145 wardens!

Checkmark

Receive the email at any hour!

Ad

Zeta Supply Inflation on Deploy Fungible Gas Coin

mediumCode4rena

Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/6a9fdc29ce7e142facfcd6f15a16bf00b659d53b/repos/node/x/fungible/keeper/gas_coin_and_pool.go#L67

Vulnerability details

Impact

The Zetachain whitepaper part 7.3 is titled Comprehensive Defense Against Arbitrary Minting. In this section, there are mentioned security protections that ensure that the supply of Zeta remains the same across all chains. Within the zetaclient, there is even zeta_supply_checker.go to ensure that this is the case. According to whitepaper, the total supply should remain the same across all chains with Ethereum being the home supply location. Finding #1 from the Halborn audit puts this as a clear risk as well.

However, this invariant is not always kept. First, the message DeployFungibleCoinZRC20 on a gas token creates a pool within SetupChainGasCoinAndPool. When creating this pool, a call to MintCoins() on the bankkeeper is made. This increases the amount of Zeta without destroying it anywhere, resulting in an inflation of ZETA tokens. This functionality is only callable by admins when new chains are created, but still breaks an important invariant within the system.

Besides increasing the ZETA tokens, it also creates some amount of the ZRC20 token, which may not exist in the TSS at the time. Having funds that are not backed by anything is very dangerous to do.

PoC

To run this PoC, do the following:

  1. Copy the code below into x/fungible/keeper/msg_server_deploy_fungible_coin_zrc20_test.go
  2. Add fmt to the list of imports at the top of the file.
  3. Within the node directory of the repo, run the following command: go test -v ./x/fungible/keeper -run TestMsgServer_IncreaseZetaAmount.
  4. Notice that the execution leads to an increase of 'azeta' without decreasing it anywhere else. This is shown by the asserts and print statements in the execution of the code.
golang
func TestMsgServer_IncreaseZetaAmount(t *testing.T) { t.Run("Deploy token increases zeta amount", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.FungibleKeeper(t) msgServer := keeper.NewMsgServerImpl(*k) k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) admin := sample.AccAddress() setAdminPolicies(ctx, zk, admin, observertypes.Policy_Type_group2) chainID := getValidChainID(t) deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) zetaAmountBefore := sdkk.BankKeeper.GetSupply(ctx, "azeta") // Deploy a gas token _, err := msgServer.DeployFungibleCoinZRC20(ctx, types.NewMsgDeployFungibleCoinZRC20( admin, sample.EthAddress().Hex(), chainID, 18, "ETH", "ETH", common.CoinType_Gas, 1000000, )) assert.Equal(t, nil, err) // Call succeeds zetaAmountAfter := sdkk.BankKeeper.GetSupply(ctx, "azeta") assert.GreaterOrEqual(t, zetaAmountAfter.Amount.Int64(), zetaAmountBefore.Amount.Int64()) fmt.Println("Supply Zeta Before:", zetaAmountBefore) fmt.Println("Supply Zeta After:", zetaAmountAfter) }) }

Remediation

To prevent the arbitrary creation of Zeta, funds should be taken from another place. Within the ecosystem, this could be done from any of the other chains, such as Ethereum and others. However, the easiest would be to take the funds from the community pool that lives on Zetachain.

This way, no funds are created out of thin air. This keeps the important invariant that the Zeta supply always stays the same throughout the entire ecosystem.

Assessed type

Math