get_the_ID() retrieves same ID on Gutenberg’s Query Loop

No, you did nothing wrong.

It’s a known issue and in WordPress v6.1.3 and up to the current stable release as of writing (v6.2.2), it’s happening because of the following lines in get_the_block_template_html() which returns the markup for the current (block-based) template:

$content = do_shortcode( $content );
$content = do_blocks( $content );

So as you could see, shortcodes in the post content (post_content) are being run before the blocks are parsed, which means that by the time your shortcode runs, the Query Loop block has not yet run, hence get_the_ID(), get_post()->ID, etc. would return the ID of the first post or whatever is the current post in the current loop.

In the past, it used to be that do_blocks() is being called before do_shortcode(), then in WordPress v6.1.2, the do_shortcode() call was removed and yet, added back in v6.1.3 (see changesets 55771 and 55830 in Trac), so I don’t know if it will ever be back to any of that behaviors.

Nonetheless, there are 2 possible workarounds you can try: