Incident,
Source: De.Fi incident database.
- Audits before it
- 2.
- Audits since
- 2, the latest dated 22 Nov 2024.
As of 3 Nov 2026, Gala has 4 audit reports by 1 security firm on Trustblock; the latest is by CertiK, dated 22 Nov 2024. Those reports list no open critical findings. It also holds 1 deployed contract on 1 chain and 1 incident (latest 19 May 2024).
Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Gala Games - TON Bridge | Security firm: CertiK | Date: | Scope: Not recorded (see the report) | 1 critical finding, fixed | Open · skynet.certik.com |
| Gala - Film | Security firm: CertiK | Date: | Scope: 1 address on Ethereum | No open critical findings | Open · skynet.certik.com |
| Gala - governance voting API | Security firm: CertiK | Date: | Scope: Not recorded (see the report) | 1 critical finding, fixed | Open · skynet.certik.com |
| Gala Games - GalaV2 | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 2 |
| High | 2 | 1 | 2 |
| Medium | 0 | 0 | 3 |
| Low | 5 | 0 | 22 |
```c=20 (int, int, int, int) ecrecover (int hash, int v, int r, int s) asm "ECRECOVER"; ... (int h, int x1, int x2, int success) = ecrecover(packet_hash, v, r, s); slice rec_pubkey = begin_cell().store_uint(h, 8).store_uint(x1, 256).store_uint(x2, 256).end_cell().begin_parse(); throw_unless(error::signature, rec_pubkey.equal_slice_bits(pubkey)); ``` `ECRECOVER` opcode [returns](https://docs.ton.org/learn/tvm-instructions/tvm-upgrade-2023-07#secp256k1) either 0 or `h x1 x2 -1`. In case 0 is returned, only **one value is pushed on stack**. Extracting 4 values will populate `(int h, int x1, int x2, int success)` with the values that were already on top of stack. These values are `r, s, v, success` completely controlled by the caller. The `success` output is **not checked** by the function. As a result, the caller can craft the signature (520 bits) that will satisfy the condition `rec_pubkey.equal_slice_bits(pubkey)` and pass the check. All funds can be stolen.
Open the reportThe "poll/cast" endpoint allows users to vote with their voting power. The number of votes the user wants to submit is referred to as the "multiplier" in the request body. Two issues are found regarding the API that, if used together, can allow the voter to have infinite voting power. * Issue 1: When a user supplies a negative "multiplier" in the API call, it will cause the "userVotesUsed" to become a negative number and effectively increase the user's total voting power. * Issue 2: Casting a vote to a non-existing option is allowed. For example, if there are only 3 options to vote on, the user can vote on option 100.
Open the reportIn `bridge_gateway` `op::route_unlock` handler the signature check is handled incorrectly: 1. It cycles `until(signatures.slice_refs_empty?())` but never updates the `signatures` with the next ref. Same signature will be used over and over again. 2. It doesn't validate the number of signatures provided. No threshold is set, one signature is always enough. 3. It doesn't ensure that the signatures are from different verifiers. Same signature can be repeated several times.
Open the reportThe "poll/cast" endpoint allows users to vote with their voting power. It was found that users can vote on a completed poll and modify the result.
Open the reportThe signed messages don't follow the [EIP-712 recommendations](https://eips.ethereum.org/EIPS/eip-712#specification). The domain separator doesn't include: 1. `name` - the user readable name of signing domain, i.e. the name of the DApp or the protocol. 2. `verifyingContract` - the address of the contract that will verify the signature. 3. `encodeType` - like "op::route_unlock(args)"h As a result, the message signed by verifiers for gateway in one chain (i.e. testnet) can be replayed on another one, or on another gateway deployment. Or the message signed with another purpose can be submitted to `op::route_unlock` handler.
Open the report`bridge_gateway` `op::route_unlock` handler doesn't check that the gas provided is enough to finish the workflow. Lack of gas can lead to incomplete transaction execution.
Open the reportThe `BLOCKLISTER_ROLE` can `blocklist()` the zero address. This would prevent all mints and burns and it may not be immediately clear what is causing the issue.
Open the reportThe data is packed in slice and parsed by the contract. To ensure the slice has the expected data structure and no data left after parsing, `end_parse()` can be applied to the slice. If slice is empty, it throws an exception.
Open the report`jetton_bridge` doesn't allow the owner to collect fees. `ton_coin_bridge` doesn't allow the owner to change the flat fee.
Open the report`load_grams()` is deprecated. `load_coins()` should be used instead.
Open the report`response_destination` in `op::transfer` message is used to accept gas excesses. Using `wallet_address` is unreasonable since it is not expected to collect extra gas.
Open the report```c=2 (_, int wallet_hash) = parse_std_addr(wallet); ``` It is not ensured the address workchain is basechain. Unexpected messages from the masterchain can break the contract invariants.
Open the report`decode_payload()` reads `extra = payload~load_maybe_ref()`. `load_maybe_ref()` loads 1 bit and if it is true, loads first ref and returns it with slice remainder. However, `encode_payload()` doesn't store that 1 bit. Trying to `decode_payload()` that was encoded by `encode_payload()` will result in cell underflow.
Open the report```c=89 amount -= flat_fee; ``` It is not checked that `amount` is bigger than `flat_fee`. As a result, the transaction will fail with integer underflow during `store_coins()`.
Open the reportSome comments are outdated. - `lock_ton#1bc9f191` is in fact `0x6372e897` - `unlock#1d1c0c53 amount:Grams instance:uint(256) recipient:^RecipientBody wallet_hash:uint(256) = InternalMsgBody;` `unlock` tag is in fact 5. The message payload is stored in the separate cell. `Grams` should be `Coins`. - etc.
Open the reportThe signature malleability is possible within the Elliptic Curve cryptographic system. An Elliptic Curve is symmetric on the X-axis, meaning two points can exist with the same `X` value. In the `r`, `s` and `v` representation this permits us to carefully adjust `s` to produce a second valid signature for the same `r`, thus breaking the assumption that a signature cannot be replayed in what is known as a replay-attack. Appendix F in the [Ethereum Yellow paper](https://ethereum.github.io/yellowpaper/paper.pdf) defines the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. ```c=144 if(v >= 31) { v = v - 31; } ``` The current code allows bigger `v` and doesn't check `s`.
Open the report`jetton_bridge::op::transfer_notification` handler works this way: 1. unpacks `op::transfer_notification` message 2. ensures `msg_value >= flat_fee` 3. unpacks the payload of `op::transfer_notification` message 4. sends `op::log_request` to the gateway However, if the `msg_value` is not enough or payload is in a wrong format, the tokens will not be returned to the owner.
Open the report### `Gala` Token - Version 1 In version 1 of the `Gala` token, the `_totalMinted` value is only updated during calls to `mint()`, where it is updated as: ```solidity _totalMinted = _totalMinted.add(amount); ``` In the hook `_beforeTokenTransfer()`, the update to this value is checked against the set `_cap` when minting: ```solidity require(totalMinted().add(amount) <= cap(), "ERC20Capped: cap exceeded"); ``` Since `_totalMinted` is only updated in `mint()` and is checked against the `_cap` value, this means that only 50 billion `GALA` will ever be minted, and that as users burn the token, the `_totalSupply` will monotonically decrease towards 0. --- ### `GalaV2` While `_totalMinted` is still updated and checked during functions `mint()` and `mintBulk()` in the same way for `GalaV2`, the value `_totalMinted` is now updated in functions `burn()` and `burnFrom()` as follows: ```solidity _totalMinted = _totalMinted - amount; ``` This means that as tokens are burned, an equivalent amount becomes available again to be minted. As such, this new update to `_totalMinted` makes the value consistent with `_totalSupply` throughout the lifetime of the `GalaV2` token. Indeed, the `_cap` on `_totalMinted` is also a `_cap` on the `_totalSupply`, making the values equivalent in all respects, within the current contract's set up.
Open the reportAccessing storage variables in a loop can be costly in terms of gas consumption. ```solidity=86 _totalMinted = _totalMinted + amounts[i]; ```
Open the reportThere is custom functionality created for accomplishing granting or revoking the `MINTER_ROLE`: `addMinter()` and `removeMinter()`. Moreover, the functions `mint()` and `mintBulk()` also perform specific checks to ensure that the `MINTER_ROLE` is also not blocklisted. Please provide more information on the types of accounts you plan to grant the `MINTER_ROLE`. Is it within the design plans of the project to allow for multiple accounts to take this role; will general users be given this privilege, where there may be a need to blocklist bad actors?
Open the reportFunction `mint()` allows for a zero input `amount`, while each `amounts[i]` must be positive in `mintBulk()`
Open the reportThe child contract uses both `msg.sender` and `_msgSender()` in its functions, while the inherited contracts consistently use `_msgSender()`. If `_msgSender()` is ever updated to be anything other than `msg.sender`, this may cause inconsistencies.
Open the reportInherited contract `PausableUpgradeable` is not initialized through call to `__Pausable_init()` in the `initialize()` function.
Open the reportFunctions `addMinter()` and `removeMinter()`, which grant the `MINTER_ROLE`, can only be called when the contract is not `_paused`; however, `grantRole()` and `revokeRole()` may still be called for any address and role, even when the contract is `_paused`.
Open the reportThe cited address input is missing a check that it is not `address(0)`.
Open the reportThe checks using the `notBlocklisted` and `whenNotPaused` modifiers are redundant in the following functions, since the function calls interact with hook `_beforeTokenTransfer()`, covering all possible cases: - `mint()`; - `mintBulk()`; - `burn()`; - `burnFrom()`
Open the reportFunctions `addMinter()`, `addUpgrader()`, `addBlocklister()`, and `transferAdmin()` all have redundant checks to modifier `onlyRole`. The aforementioned functions specify that only the `DEFAULT_ADMIN_ROLE` may call these functions; by default, this is the `adminRole` assigned to all roles. The redundancy also exists within functions `removeMinter()`, `removeUpgrader()`, and `removeBlocklister()`. In the case where `DEFAULT_ADMIN_ROLE` account calls a function like `addMinter()`, the check on the `_msgSender()` confirming they are the `DEFAULT_ADMIN_ROLE` is performed twice; once at the beginning of `addMinter()` and once at the execution of `grantRole()`. --- It is noted that if the contract is ever upgraded, each `role` may be assigned an `adminRole` distinct from `DEFAULT_ADMIN_ROLE`. This can be done through adding additional functionality which interacts with internal function `_setRoleAdmin()`. For example, it is possible that another "MinterAdmin" is assigned separate from the `DEFAULT_ADMIN_ROLE` as the `adminRole` for `MINTER_ROLE`. In such a case, this "MinterAdmin" would be able to call `grantRole()` and `revokeRole()` directly, but would not be able to call function `addMinter()`, since this function specifies that only `DEFAULT_ADMIN_ROLE` may perform the call. Furthermore, `DEFAULT_ADMIN_ROLE` would cease to be able to call `addMinter()` in this instance.
Open the reportThe use of the `super` keyword is not necessary since there is no override and there is a well-defined path to the internal `_pause()` and `_unpause()` functions inherited from `ERC20PausableUpgradeable`, and to the public `hasRole()` function `from `AccessControlUpgradeable`.
Open the reportThe variable `_cap` assigned in the initializer is never modified throughout the contract and is a hardcoded value. Instead, the variable can be initialized before the function call, with keyword `constant`, to optimize gas usage.
Open the reportAttached through a shared contract address. Our incident data ends in Oct 2026.
Source: De.Fi incident database.
| Address | Chain | Source code |
|---|---|---|
| 0xacc3…15cd0xacc36ebdc4b394f0b03ee2bbd1a6b701c0ab15cd | Chain: Ethereum | Source code: Verified source |
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="gala-games"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us