As of 3 Nov 2026, XP Network has 2 audit reports by 1 security firm on Trustblock; the latest is by CertiK, dated 8 May 2023. Those reports list no open critical findings.
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 |
|---|---|---|---|---|---|
| XP Network - Dfinity Integration | Security firm: CertiK | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · skynet.certik.com |
| TON-Ethereum Bridge | Security firm: CertiK | Date: | Scope: Not recorded (see 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 | 0 |
| High | 0 | 2 | 1 |
| Medium | 0 | 0 | 3 |
| Low | 0 | 0 | 5 |
The `withdraw_nft()` function, which is defined as an external function accessible by any account on the ICP chain, unlocks the source NFT which was locked on an external chain by burning wrapped NFT on the ICP chain. However, its implementation lacks verification codes for amount of NFT transaction fee and ownership of wrapped NFT, allowing malicious users to call the `withdraw_nft()` function with fake arguments and obtain the source NFT on the external chain for only the ICP transfer fee.
Open the reportFrom a design perspective, the `FEEBLOCK_STORE` variable was created to prevent duplicate fee_block submissions. ```rust async fn require_tx_fee( canister_id: &Principal, caller: &Principal, fee_block: BlockIndex, ) -> Result<u64, BridgeError> { if FEEBLOCK_STORE.with(|store| store.borrow().contains(&fee_block)) { return Err(BridgeError::InvalidFee); } ... } ``` Since the function does not store `fee_block` into `FEEBLOCK_STORE`, `FEEBLOCK_STORE` is always empty, which means that this check is meaningless. If the fee oracle can guarantee that the incoming `fee_block` will never be repeated, then there is no problem. But we think the best way to deal with it is to store the verified fee_block in `FEEBLOCK_STORE`. In this way, every time a new `fee_block` comes in, the requirement can check whether it already exists in the `FEEBLOCK_STORE`, thus ensuring that the same `fee_block` is not submitted repeatedly.
Open the reportIn the execution of a transfer operation within the [Ledger Canister](https://github.com/dfinity/ledger-ref/blob/3ed0206afd231b9586c9e7b9a793948f10118297/src/Ledger.mo#L209), a fee must be paid, and it will check if the source account holds a sufficient amount of ICP to cover the transfer amount plus the fee. In the rust code, the logic is to withdraw all the ICP balance from the `minter` contract, and the `amount` passed to the `transfer` method is equal to the total ICP balance, with a standard fee of 10^-4 ICP. The sum of the `amount` and `fee` will exceed the amount of ICP held by the `minter` contract, resulting in the transfer failing. The actual transaction amount should be `bal- fee`.
Open the reportThere will be a potential infinite loop for method `clean_logs()` in the `minter` canister.\ In that method, there is no validation for input parameter `action`.\ If `action.from_action` is bigger than `action.to_action`, it will case the not intended looping at L401. ```rust=401 while action.from_action != action.to_action { bmap.remove(&action.from_action); action.from_action += Nat::from(1u32); } ```
Open the reportThe `unsafe` block in Line 58 is duplicate and could be replaced by calling function `config_ref()`. ```rust=58 let conf = unsafe { CONFIG.as_ref().unwrap() }; ```
Open the reportLoop iterates over `action.token_urls[i]` vector (i=0... size of token_urls) and accesses `action.mint_with[i]` vector without asserting that `action.token_urls` and `action.mint_with` have equal lengths. ```rust=461 for (i, token_url) in action.token_urls.into_iter().enumerate() { xpnft_mint(action.mint_with[i], token_url, action.to) ``` If the length of `action.mint_with` is less than `action.token_urls`, a panic will be caused due to the subscript being out of bounds. The similar issue also exists in the function `validate_unfreeze_nft_batch()`.
Open the reportThe logic of `clean_logs()` function is to delete a specified `action` item. If the designated `action` number does not exist, the `clean_logs()` function will not throw an error, but it will waste Cycles. To avoid the pointless consumption of Cycles, it is necessary to check the existence of the `action` number.
Open the reportIt is unnecessary to use `clone` as this value is dropped without further use. ```rust=152 let key = PublicKey::new(config_ref().group_key.clone()); ```
Open the reportThe above line creates new references, which are immediately dereferenced by the compiler. ```rust=207 let caller_acc = AccountIdentifier::new(&caller, &DEFAULT_SUBACCOUNT); let canister_acc = AccountIdentifier::new(&canister_id, &DEFAULT_SUBACCOUNT); ``` However, since the `caller` and `canister_id` are already references of the `Principal` type, which meet the parameter type requirements of the function, there is no need to create new references again.
Open the reportThe reports name a repository or files. Open a report to see its scope.
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="xpnetwork"></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