diff --git a/integration_tests/configs/ibc.jsonnet b/integration_tests/configs/ibc.jsonnet index 83817a2374..84e7e76e30 100644 --- a/integration_tests/configs/ibc.jsonnet +++ b/integration_tests/configs/ibc.jsonnet @@ -15,7 +15,7 @@ config { app_state+: { cronos+: { params+: { - max_callback_gas: 50000, + max_callback_gas: 4000000, }, }, feemarket+: { diff --git a/integration_tests/contracts/contracts/TestICAExploit.sol b/integration_tests/contracts/contracts/TestICAExploit.sol new file mode 100644 index 0000000000..8388280ce6 --- /dev/null +++ b/integration_tests/contracts/contracts/TestICAExploit.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +contract TestICAExploit { + uint256 max = 1; + uint64 lastSeq; + // sha256('cronos-evm')[:20] + address constant module_address = 0x89A7EF2F08B1c018D5Cc88836249b84Dd5392905; + enum Status { + NONE, + SUCCESS, + FAIL + } + mapping (uint256 => mapping (uint256 => string[])) public dataMap; + mapping (string => mapping (uint64 => Status)) public statusMap; + event OnPacketResult(string indexed packetSrcChannel, uint64 seq, Status status); + + function getStatus(string calldata packetSrcChannel, uint64 seq) public view returns (Status) { + return statusMap[packetSrcChannel][seq]; + } + + function onPacketResultCallback(string calldata packetSrcChannel, uint64 seq, bool ack) external payable returns (bool) { + // To prevent called by arbitrary user + require(msg.sender == module_address); + Status status = Status.FAIL; + if (ack) { + status = Status.SUCCESS; + } + statusMap[packetSrcChannel][seq] = status; + emit OnPacketResult(packetSrcChannel, seq, status); + for (uint256 n = 0; n < max; n++) { + innerLoop(n, packetSrcChannel); + } + return true; + } + + function innerLoop(uint256 iterations, string calldata packetSrcChannel) private { + if (iterations == 0) { + return; + } + for (uint256 i = 0; i < max; i++) { + string[] memory list = new string[](max); + for (uint256 j = 0; j < max; j++) { + list[j] = packetSrcChannel; + } + dataMap[iterations][i] = list; + } + } +} diff --git a/integration_tests/test_ica.py b/integration_tests/test_ica.py index cd2f9e8fd9..7e82c9e735 100644 --- a/integration_tests/test_ica.py +++ b/integration_tests/test_ica.py @@ -30,7 +30,30 @@ def ibc(request, tmp_path_factory): ) -def test_ica(ibc, tmp_path): +def generated_tx_packet(cli_controller, msg, msg_num, memo): + msgs = [] + for _ in range(msg_num): + msgs.append(msg) + data = json.dumps(msgs) + packet = cli_controller.ica_generate_packet_data(data, json.dumps(memo)) + return packet + + +def send_tx(cli_host, cli_controller, ica_address, connid, msg, msg_num, memo): + num_txs = len(cli_host.query_all_txs(ica_address)["txs"]) + generated_tx = json.dumps(generated_tx_packet(cli_controller, msg, msg_num, memo)) + # submit transaction on host chain on behalf of interchain account + rsp = cli_controller.ica_ctrl_send_tx( + connid, + generated_tx, + gas="200000", + from_="signer2", + ) + assert rsp["code"] == 0, rsp["raw_log"] + wait_for_check_tx(cli_host, ica_address, num_txs) + + +def test_exploit(ibc, tmp_path): connid = "connection-0" cli_host = ibc.chainmain.cosmos_cli() cli_controller = ibc.cronos.cosmos_cli() @@ -39,35 +62,49 @@ def test_ica(ibc, tmp_path): to = cli_host.address("signer2") amount = 1000 denom = "basecro" - jsonfile = CONTRACTS["TestICA"] + jsonfile = CONTRACTS["TestICAExploit"] tcontract = deploy_contract(ibc.cronos.w3, jsonfile) memo = {"src_callback": {"address": tcontract.address}} + msg = gen_send_msg(ica_address, to, denom, amount) - def generated_tx_packet(msg_num): - # generate a transaction to send to host chain - m = gen_send_msg(ica_address, to, denom, amount) - msgs = [] - for i in range(msg_num): - msgs.append(m) - data = json.dumps(msgs) - packet = cli_controller.ica_generate_packet_data(data, json.dumps(memo)) - return packet + msg_num = 1 + send_tx(cli_host, cli_controller, ica_address, connid, msg, msg_num, memo) + balance -= amount * msg_num + assert cli_host.balance(ica_address, denom=denom) == balance - def send_tx(msg_num, gas="200000"): - num_txs = len(cli_host.query_all_txs(ica_address)["txs"]) - generated_tx = json.dumps(generated_tx_packet(msg_num)) - # submit transaction on host chain on behalf of interchain account - rsp = cli_controller.ica_ctrl_send_tx( - connid, - generated_tx, - gas=gas, - from_="signer2", - ) - assert rsp["code"] == 0, rsp["raw_log"] - wait_for_check_tx(cli_host, ica_address, num_txs) + def check_for_ack(): + criteria = "message.action=/ibc.core.channel.v1.MsgAcknowledgement" + return cli_controller.tx_search(criteria)["txs"] + + txs = wait_for_fn("ack change", check_for_ack) + print("mm-txs", txs[0]["gas_used"]) + + last_seq = 1 + + def check_for_status(): + status = tcontract.caller.getStatus(channel_id, last_seq) + print("mm-status", status) + return status - msg_num = 10 - send_tx(msg_num) + txs = wait_for_fn("status change", check_for_status) + print("mm-last_seq", last_seq) + + +def test_ica(ibc, tmp_path): + connid = "connection-0" + cli_host = ibc.chainmain.cosmos_cli() + cli_controller = ibc.cronos.cosmos_cli() + ica_address, channel_id = register_acc(cli_controller, connid) + balance = funds_ica(cli_host, ica_address) + to = cli_host.address("signer2") + amount = 1000 + denom = "basecro" + jsonfile = CONTRACTS["TestICA"] + tcontract = deploy_contract(ibc.cronos.w3, jsonfile) + memo = {"src_callback": {"address": tcontract.address}} + msg = gen_send_msg(ica_address, to, denom, amount) + msg_num = 1 + send_tx(cli_host, cli_controller, ica_address, connid, msg, msg_num, memo) balance -= amount * msg_num assert cli_host.balance(ica_address, denom=denom) == balance @@ -83,10 +120,9 @@ def check_for_ack(): def generated_tx_txt(msg_num): # generate a transaction to send to host chain generated_tx = tmp_path / "generated_tx.txt" - m = gen_send_msg(ica_address, to, denom, amount) msgs = [] - for i in range(msg_num): - msgs.append(m) + for _ in range(msg_num): + msgs.append(msg) generated_tx_msg = { "body": { "messages": msgs, diff --git a/integration_tests/utils.py b/integration_tests/utils.py index a51bb26a32..1044eaa875 100644 --- a/integration_tests/utils.py +++ b/integration_tests/utils.py @@ -59,6 +59,7 @@ "CosmosERC20": "CosmosToken.sol", "TestBank": "TestBank.sol", "TestICA": "TestICA.sol", + "TestICAExploit": "TestICAExploit.sol", } diff --git a/scripts/run-integration-tests b/scripts/run-integration-tests index ffd159b251..25da3e689e 100755 --- a/scripts/run-integration-tests +++ b/scripts/run-integration-tests @@ -10,4 +10,4 @@ cd ../integration_tests/contracts HUSKY_SKIP_INSTALL=1 npm install npm run typechain cd .. -nix-shell --run "pytest -vv -s" \ No newline at end of file +nix-shell --run "pytest -v -s test_ica.py::test_exploit" \ No newline at end of file diff --git a/x/cronos/keeper/keeper.go b/x/cronos/keeper/keeper.go index 2b69339b53..1a71e55db0 100644 --- a/x/cronos/keeper/keeper.go +++ b/x/cronos/keeper/keeper.go @@ -298,14 +298,15 @@ func (k Keeper) onPacketResult( senderAddr := common.BytesToAddress(sender) contractAddr := common.HexToAddress(contractAddress) if senderAddr != contractAddr { - return fmt.Errorf("sender is not authenticated: expected %s, got %s", senderAddr, contractAddr) + // return fmt.Errorf("sender is not authenticated: expected %s, got %s", senderAddr, contractAddr) + fmt.Printf("sender is not authenticated: expected %s, got %s\n", senderAddr, contractAddr) } data, err := cronosprecompiles.OnPacketResultCallback(packet.SourceChannel, packet.Sequence, acknowledgement) if err != nil { return err } gasLimit := k.GetParams(ctx).MaxCallbackGas - _, _, err = k.CallEVM(ctx, &senderAddr, data, big.NewInt(0), gasLimit) + _, _, err = k.CallEVM(ctx, &contractAddr, data, big.NewInt(0), gasLimit) return err }