Zeta Supply Inflation on Deploy Fungible Gas Coin
mediumLines of code
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:
- Copy the code below into
x/fungible/keeper/msg_server_deploy_fungible_coin_zrc20_test.go - Add
fmtto the list of imports at the top of the file. - Within the
nodedirectory of the repo, run the following command:go test -v ./x/fungible/keeper -run TestMsgServer_IncreaseZetaAmount. - 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.
golangfunc 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
