TL;DR
On 19 September 2026 at 20:21:47 UTC an attacker called conversionIn on SingularityNET's TokenConversionManagerV3 contract with a valid authorizer signature and walked out with 8,721,530 FET, the converter's entire float, in a single transaction. Twenty-eight minutes later a stolen NuNet minter role produced 408,532,878 NTX. Seven hours after that, 260,000,000 AGIX and 53,838,000 WMTx followed. PeckShield put the attacker's holdings at roughly $16.77 million; the realized take is closer to $2.25 million, because the large line in that total is now frozen behind a token pause. The contract that lost everything stored a 1,000,000 per-transaction ceiling in its own state and never applied it to the payout path. That is the whole story: the key decided who could move money, and the cap decided how much. Only one of those two controls was actually wired up.
What Happened on 19 September?
An attacker held valid signing material for the Ethereum side of the SingularityNET bridge. Everything downstream follows from that.
We pulled the chain state ourselves rather than relying on the reporting. Transaction 0xfe12c63b...62ac69e2 landed in Ethereum block 26,013,913 at 20:21:47 UTC. Caller 0x1572f2af...0f86c362. Target 0xab424A43...05ADF3A3, the TokenConversionManagerV3 contract. Selector 0x6a02fcc4, which is conversionIn(address,uint256,bytes32,uint8,bytes32,bytes32). Recipient 0x2dcc1085...37cc21dfE. Amount 8,721,530.40 FET. Gas used 89,002. Status success.
Call getConverterBalance() today and it returns zero. One call took the float.
A converter is the Ethereum leg of a token bridge. Users lock tokens on one chain, an off-chain service watches for that lock and signs a message letting them claim the matching tokens on the other chain. The signature is the whole security model: the contract does not know what happened on Cardano, only whether the message was signed by the address it trusts. Steal that key and you are the bridge.
At 20:50:11 UTC, 28 minutes and 24 seconds later, transaction 0xe14442f6...03ee13ab minted 408,532,878 NTX to the same recipient wallet. NuNet's token is a standard role-gated ERC-20, and we confirmed that the deployer address 0x863F13e5...80092165 held MINTER_ROLE at that exact block.
The AGIX leg waited for the following morning. Between 03:13:35 and 03:36:23 UTC on 20 September, a run of mint transactions moved 260,000,000 AGIX from the zero address into the attacker's wallet. Two minutes later, a third address began working a second converter for WMTx.
Why Did One Signature Empty the Whole Converter?
Because the contract had a spending limit and did not check it on the way out.
TokenConversionManagerV3 is verified source on Sourcify, so this is readable by anyone. It defines a checkLimits modifier that reverts when an amount falls outside _perTxnMinAmount and _perTxnMaxAmount. Call getConversionConfigurations() on the deployed contract and it returns those two values: 100 FET and 1,000,000 FET. That read is ours, reproducible by anyone against the address above. Both configured, both live, both sitting in storage at the moment of the drain.
Now look at where the modifier is applied:
// conversionOut: user locks tokens, leaving Ethereum
function conversionOut(uint256 amount, bytes32 conversionId, uint8 v, bytes32 r, bytes32 s)
external
checkLimits(amount) // <-- cap enforced
// conversionIn: contract pays tokens out, arriving on Ethereum
function conversionIn(address to, uint256 amount, bytes32 conversionId, uint8 v, bytes32 r, bytes32 s)
external
notZeroAddress(to) // <-- that is the entire guardThe cap governs the direction that takes money from a user. The direction that hands money to a user runs uncapped. conversionIn checks the signature, checks for replay, checks that the contract holds enough tokens, and transfers. Its only ceiling is the contract's balance, which matches Cryptopolitan's reading of a function that accepts one account's signature as its only authorization and skips the spending-limit check present elsewhere in the same contract.
8,721,530 FET against a stored maximum of 1,000,000 FET is 8.72 times the limit the contract itself was configured to enforce.
This is a regression, and the previous version is also on-chain and also verified. TokenConversionManagerV2 at 0x63BBC71C...8a35d392 declares its payout function as external payable checkLimits(amount) nonReentrant notZeroAddress(to). V3 dropped both checkLimits and nonReentrant from the same function. The unguarded payout path had been live and holding a float for as long as V3 existed, waiting for someone with a key to point at it. Whatever the reason for the rewrite, the newer contract is the weaker one.
The digest has a second problem. conversionIn hashes "__conversionIn", the amount, msg.sender, the conversion id and the contract address. The to parameter is absent, so a signature authorizes an amount to whoever the caller picks. The contract's own NatSpec describes that parameter as the destination address the converted tokens are transferred to, which reads as a binding commitment the code never makes. It changed nothing here, since the attacker held the key anyway, but any user with a valid conversion signature can redirect their own payout.
The Attack, Step by Step
| # | Time (UTC) | Chain action | Result |
|---|---|---|---|
| 1 | 19 Sep 20:21:47 | conversionIn on V3, block 26,013,913 | 8,721,530 FET paid out, converter float goes to zero |
| 2 | 19 Sep 20:50:11 | NTX mint via stolen MINTER_ROLE, block 26,014,055 | 408,532,878 NTX created, supply reaches 96.7% of MAX_SUPPLY |
| 3 | 19 Sep 23:47 to 20 Sep 02:16 | NTX sold down in tranches | NTX converted to ETH before the pause |
| 4 | 20 Sep 03:13:35 to 03:36:23 | AGIX mint transactions | 260,000,000 AGIX created |
| 5 | 20 Sep 03:25 to 04:13 | 61.7M AGIX sold | remaining 198.3M held |
| 6 | 20 Sep 03:38:23 to 03:45:47 | 54 conversionIn calls on V2, each requesting exactly 1,000,000 | 53,838,000 WMTx delivered net of commission |
| 7 | 20 Sep 08:51:11 | NuNet pauses NTX, block 26,017,641 | NTX transfers halted |
| 8 | 20 Sep 09:51:47 | AGIX paused, block 26,017,942 | 198.3M AGIX frozen in the attacker's wallet |
| 9 | 21 Sep 12:22:59 | NuNet revokes MINTER_ROLE, block 26,025,859 | mint path closed |
Three different caller addresses ran those steps, and two different converter authorizer keys signed them, one for V3 and one for V2. Add the NuNet minter role and that is three signing identities exercised inside eight hours. One stray leaked key does not look like that. A compromised signing host does. A Bitquery-derived analysis describes the V3 authorizer as an offline nonce-zero key, an address that had never sent a transaction of its own, which puts the theft in key custody or the signing service rather than in a hot wallet. No project in the group has published a root cause for the compromise itself as of 23 September, so treat the exact vector as unexplained.
Why Did the Three Tokens Lose Different Amounts?
Same attacker, same night, same class of stolen authority. Three ceilings, three outcomes.
FET had a cap that was never applied to payouts, so the attacker took 100% of the float in one call.
WMTx had the same cap applied. Every one of the 54 calls we traced requested exactly 1,000,000 WMTx, the configured maximum, and delivered 997,000 after commission. The cap held on every transaction. It ran for seven minutes and 24 seconds and the attacker collected 53,838,000 anyway. A per-transaction limit with no rate limit behind it is a speed bump, which CWE-799 calls improper control of interaction frequency.
NTX had no per-transaction cap at all, but its verified ERC-20 source declares MAX_SUPPLY as 1,000,000,000 and refuses any mint past it, against a current total supply of 966,647,334. Pre-mint supply was 558,114,456. The attacker minted 408,532,878, which is 92.4% of every token that could still legally exist. The hard cap was the only control in the room saying no, and it said no at 96.7% of the ceiling.
The ceiling set the loss, not the key. The attacker's authority was total in all three cases; what varied was the bound, and only NTX had one that operated on the cumulative position rather than a single call. CWE-770 is the parent class for the V3 failure: an allocation path with no limit or throttling.
A per-transaction limit describes the shape of one call, and an attacker with a valid signature can make as many calls as gas allows.
Is the $16.77M Figure Real?
No. Most of it cannot be moved, and the gap is instructive.
PeckShield's tally breaks into 198.3M AGIX at about $14.42 million, 649 ETH at about $1.67 million, and 33.538M WMTx at about $627,350. We confirmed the token positions exactly. Of the 53,838,000 WMTx that arrived, 20,300,000 was sold and 33,538,000 remains, which is where PeckShield's WMTx line comes from. AGIX runs the same way: 260,000,000 minted, 61,700,000 sold, 198,300,000 still sitting in the wallet. The ETH line has moved. It stands at 433.05 ETH today, roughly 216 ETH below the reported figure, with 100 ETH leaving in batches as recently as 22 September at 01:42 UTC.
The AGIX line is the problem with the headline. AGIX's paused() returned false through the mints and the sales, and we binary-searched the transition: it flipped true at block 26,017,942, 20 September 09:51:47 UTC. The attacker sold 61.7M AGIX before that and has been sitting on 198.3M since. Valuing a frozen position in a token that fell about 99% in 24 hours, per the same analysis, at its pre-collapse mark describes nothing the attacker can act on.
The proceeds move in a pattern worth naming. Token sales routed through 0x74de5d4F...94016631, the allowance target the explorer labels Spender. Its verified source is a small contract named Spender whose entry points we read as swap and metaswap, the shape of the MetaMask Swaps spender. If that reading holds, the tokens went out through a retail wallet's swap button. The ETH then leaves in exact 100 ETH units to freshly funded addresses with no other history, and those addresses pass the same 100 ETH between each other: 0xC5E7cB51...Aa5F51bD sent 100 ETH to 0xB312D8fd...971a647E on 21 September at 00:44:23 UTC and got it back three hours later. One leg reached a contract the explorer labels only Deposit, with no public attribution, so we will not name a venue. Peel-chain behaviour, unhurried, and still running two days after the theft.
Protos puts realized proceeds at roughly $2.25 million: 523 ETH from the FET sales and 183 ETH from everything else. That is the figure we would use, and it is the high end of the range. Cryptopolitan values the two largest legs at about $1.53 million of FET and $452,000 to $463,000 of NTX, which totals near $2 million and leaves roughly a 12% gap against Protos. The gap is the smaller sales, and nobody has published a line-by-line ledger that closes it.
One reported number does not survive the chain. Protos describes 900 million AGIX and 500 million each of WMTx and CGV as minted. We summed every zero-address transfer into the attacker's wallet and got 260,000,000 AGIX exactly, matching the PeckShield figure Metaverse Post carried rather than the larger tally. WMTx reached the wallet as 53,838,000, and not as a mint at all: it came through the V2 converter in 54 pieces. Where the two accounts disagree, we trust the transfer log.
What Is Still Live?
NuNet responded. NTX was paused at 20 September 08:51:11 UTC, 12 hours and one minute after the mint, and the deployer's MINTER_ROLE was revoked at 21 September 12:22:59 UTC. We checked the role at both blocks: held at the mint, gone now. Metaverse Post's 21 September report that the minter role was still live was accurate when written and has since been overtaken.
The converter has not been fixed. We read getConversionAuthorizer() on the verified contract above at block 26,013,913 and again at block 26,038,357 on 23 September, and both return the same address. The key that signed the drain is still the key the contract trusts, four days later, which extends what the Bitquery-derived analysis found at the 48-hour mark. It is a plain externally owned account, not a multisig, while the contract's owner is a contract. The admin role got the Safe and the money-moving role got an EOA.
Nothing is at risk today because the float is zero. The path re-arms the moment anyone calls increaseConverterLiquidity. Fetch.ai has paused AGIX to FET conversions and stated its own contracts were unaffected, which the chain supports: FET's token contract was never touched, only the converter's balance. Rotating the authorizer before that pause lifts is the difference between a closed incident and a scheduled one.
What Should Operators Change?
-
Apply your limits to the outbound path, not just the inbound one. The V3 converter stored a 1,000,000 FET maximum and checked it only on deposits. Read every privileged function in your own contracts and list the modifiers each one carries. If the function that pays out has fewer guards than the one that takes in, you have this bug.
-
Diff your upgrades against the version they replace. V2's payout function carried
checkLimitsandnonReentrant. V3's carries neither. A rewrite that drops a modifier is the easiest finding in a security review and the easiest thing to miss when the new version passes its own tests. We could not find a published audit report covering V3, or any public changelog naming who made the change. That does not mean no review happened, only that nothing public documents anyone looking at this function before the drain. -
Cap the cumulative, not the call. The WMTx converter enforced its per-transaction limit perfectly and lost 53.8 million tokens to a loop. Add a rolling window: total converted per hour, per day, per authorizer. NTX's
MAX_SUPPLYwas the only control that night repetition could not defeat. -
Give the signing key the governance you gave the owner. A single EOA authorized every payout on a bridge whose admin was a contract. Threshold signatures, an HSM, or a second signer above an amount floor all cost less than one incident.
-
Bind the signature to the recipient. Put
toin the signed digest and add a deadline, so a leaked signature is scoped to one destination and stops being valid. -
Rotate before you unpause. Reopening conversions while the contract still trusts the compromised authorizer restarts the incident with a fresh float.
Where Monitoring Fits, Honestly
Alerting does not stop the first transaction. This one was a single call, mined in a single block, and nothing fires ahead of it. What alerting changes here is the clock, and the clock was where the money actually went.
Four events here are alarmable against figures the contracts publish about themselves. At 20:21:47 a ConversionIn event fired for 8,721,530 FET against a perTxnMaxAmount of 1,000,000 FET stored in the same contract: a rule comparing an event amount to the contract's own ceiling fires on transaction one. In the same block the converter balance went to zero. At 20:50:11 one mint took NTX supply from 558,114,456 to 966,647,334, or 96.7% of a MAX_SUPPLY the token declares publicly. From 03:38:23 the next morning, 54 conversions landed in seven minutes and 24 seconds, every one exactly at the cap.
The AGIX mints did not begin until 03:13:35 on 20 September, six hours and 52 minutes after the first of those signals, and the pause arrived at 09:51:47, six hours after the 260 million AGIX already existed. Every AGIX token here was minted inside a window that two earlier alarms had already opened. Tripwire watches contract state and event flow against the limits a protocol declares for itself, so a payout exceeding a stored cap, a supply jumping toward a hard ceiling, or a converter emptying in one block raises an alert while the attacker is still working. Six hours and 52 minutes is the margin the first two signals actually bought, and the 260 million AGIX was minted inside it.
The missing cap is a different lane. conversionIn losing checkLimits between V2 and V3 is a diff a reviewer reads, and our full audit work exists to catch that class of regression before a key theft turns it into a total loss. We saw the same shape in AFX Trade's bridge validator compromise: the key theft is the event everyone reports, and the missing bound decides the size of the hole.
Frequently Asked Questions
Was this a smart contract bug or a stolen key?
Both. The attacker needed a stolen authorizer key to call conversionIn at all, so no code review prevents the first transaction. The contract's own per-transaction maximum would have capped it at 1,000,000 FET instead of 8,721,530, and it was never applied to the payout path.
Were Fetch.ai's own contracts compromised?
No. FET's token contract was untouched. What emptied was the FET balance held by SingularityNET's TokenConversionManagerV3, a separate contract with its own authorizer. Fetch.ai paused AGIX to FET conversions and stated its contracts were not under threat, which the chain supports.
Can the attacker do it again?
Not right now, and only because the converter holds zero FET. Its authorizer is unchanged from the exploit block by our own read on 23 September, so the same signature path works the moment liquidity returns.
Is the attacker's $16.77 million spendable?
No. About $14.42 million of it is 198.3 million AGIX, and AGIX transfers have been paused since 20 September 09:51:47 UTC. The attacker realized roughly $2.25 million before the pauses landed and holds 433.05 ETH. Treat $16.77 million as a mark on frozen inventory in a token that fell about 99%.
How many keys were actually compromised?
At least three signing identities: the V3 converter authorizer, the V2 converter authorizer, and NuNet's MINTER_ROLE. Distinct addresses, all exercised within eight hours, which points at a compromised signing environment rather than one leaked secret. No project has published a root cause, so that inference is ours.
Sources / References
- Protos, "$2M stolen in triple attack on Fetch.ai, NuNet, and SingularityNET": https://protos.com/2m-stolen-in-triple-attack-on-fetch-ai-nunet-and-singularitynet/
- The Crypto Times, "SingularityNET Bridge Hack Widens: 260M AGIX, 53.8M WMTx Minted, $16.77M Held by Attacker": https://www.cryptotimes.io/2026/09/21/singularitynet-bridge-hack-widens-260m-agix-53-8m-wmtx-minted-16-77m-held-by-attacker/
- Cryptopolitan, "Fetch.ai and NuNet hit in attacks linked to a compromised private key": https://www.cryptopolitan.com/fetch-ai-and-nunet-hit-in-attacks-linked-to-a-compromised-private-key/
- Metaverse Post, "Key Compromise Behind Fetch.ai-Linked Bridge Attack Drives Losses To $16.77M": https://mpost.io/key-compromise-behind-fetch-ai-linked-bridge-attack-drives-losses-to-16-77m/
- The Cryptonomist, "SingularityNET contract exploit forces Fetch.ai to pause AGIX-FET conversions": https://en.cryptonomist.ch/2026/09/20/singularitynet-contract-exploit/
- Bitquery-derived analysis, "SingularityNET Bridge Key Hack Drains $16.77M": https://github.com/Ricosworks1/blockchain-payment-flow-analysis/releases/tag/deep-dive-singularitynet-bridge-hack-16m-key-compromise-sept-2026
- Sourcify verified source,
TokenConversionManagerV3: https://repo.sourcify.dev/1/0xab424A430CC09864fA1277A38193111705ADF3A3 - Sourcify verified source, MetaMask Swaps
Spender: https://repo.sourcify.dev/1/0x74de5d4FCbf63E00296fd95d33236B9794016631 - Sourcify verified source,
TokenConversionManagerV2: https://repo.sourcify.dev/1/0x63BBC71C034bC99A905d5594dF985C218a35d392 - Blockscout, FET drain transaction: https://eth.blockscout.com/api/v2/transactions/0xfe12c63b322d52727c615f3342222138d1563400a9880cebb516a9a162ac69e2
- Blockscout, NTX mint transaction: https://eth.blockscout.com/api/v2/transactions/0xe14442f6171d8a652e79d44336e58c00cdab271bdb69c668493d420e03ee13ab
- Blockscout, NuNet
NuNetTokenverified source: https://eth.blockscout.com/api/v2/smart-contracts/0xF0d33BeDa4d734C72684b5f9abBEbf715D0a7935 - Blockscout, NTX token supply: https://eth.blockscout.com/api/v2/tokens/0xF0d33BeDa4d734C72684b5f9abBEbf715D0a7935
- Blockscout, AGIX transfer log for the attacker wallet: https://eth.blockscout.com/api/v2/addresses/0x2dcc1085fDCf418B421E45e86e4e54637cc21dfE/token-transfers?type=ERC-20&token=0x5B7533812759B45C2B44C19e320ba2cD2681b542
- MITRE CWE-770, Allocation of Resources Without Limits or Throttling: https://cwe.mitre.org/data/definitions/770.html
- MITRE CWE-799, Improper Control of Interaction Frequency: https://cwe.mitre.org/data/definitions/799.html



