In February 2025, nearly $1.5B was stolen from the Bybit exchange in what has been called the largest cryptocurrency hack in history. Paradoxically, it wasn’t enabled by a smart contract vulnerability, but by social engineering. Given our experience with auditing Safe’s smart contracts, which Bybit uses, we decided to investigate the breach in more detail.
TLDR: Projects using Safe Wallets – especially those managing large funds – need to actively configure built-in security features like Safe Guards and timelocks. These features exist for a reason.
What happened?
Here’s how the incident unfolded (timeline by BlockThreat):
- The attacker first compromised the development machine of a single Safe developer. This granted access to an AWS session key, though initially it didn’t allow changes to the frontend.
- Over the span of two weeks, the attacker mimicked this developer’s online activity patterns and probing for weaknesses in AWS security.
- Using a time-limited AWS key and 2FA confirmation (using the compromised developer’s credentials), the attacker was eventually able to deploy malicious code to the Safe frontend.
- The attacker injected malicious frontend code that generated a targeted signature request specifically crafted for the Bybit account.
- It’s likely that the attacker used social engineering to identify that Bybit signers didn’t properly verify transactions on their hardware wallets. This allowed malicious signature requests to slip through.
- The final step required signatures from three Bybit cold storage signers. Through the compromised Safe frontend, they were likely shown a benign-looking transaction – but in reality, it executed a contract upgrade using
delegatecall
, swapping in a malicious implementation. - With control of the vault, the attacker drained all assets. The address and related transactions can be viewed on Etherscan.
How could have it been prevented?
Let’s take a closer look at the security features Safe offers in mitigating smart contract security risks.
The most critical issue to the hack was blind-signing, a longstanding problem in the ecosystem. Cold wallets often have poor UX for reviewing transactions, making it easy for signers to approve malicious payloads during routine operations without validating what they’re actually signing.
Thankfully, there are tools designed to address this. One example is the Safe script validator, originally built by @pcaversaccio and now hosted by OpenZeppelin. This tool allows signers to verify the payload being signed, byte-by-byte, against the expected Safe script before confirming it on a hardware wallet.
Beyond user-level tooling, there’s also room to improve multisig thresholds and introduce automatic payload validation at the signing machine level, reducing the risk of human error.
We should also look beyond Web2-style defenses. Safe offers Safe Guards, built-in on-chain security protocols which, if properly configured, could have outright prevented the loss of Bybit’s funds. Despite being available, they’re often left unused or misunderstood. That needs to change.
Hardening multisig with Safe Guards
Safe Wallet can be natively extended by a Safe Module or Safe Guard. Modules allow arbitrary conditions for execution from Safe (based on the Module logic), and multiple Modules can be defined for one wallet. There is always one Guard, and it can only block transactions. We have already described security best practices around Safe in a recent blog post and also discussed it at SafeCon 2023 in Berlin. Let’s look at how Guards can help us secure our wallets.
As stated in the official documentation: “Safe Guards are used when there are restrictions on top of the n-out-of-m scheme”, letting us limit certain operations on-chain. Safe Guards provide us, by design, pre-checks and post-checks, and maintain their own state. A great example of Safe Guard is ScopeGuard:
function checkTransaction(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256,
uint256,
uint256,
address,
// solhint-disallow-next-line no-unused-vars
address payable,
bytes memory,
address
) external view override {
require(
operation != Enum.Operation.DelegateCall ||
allowedTargets[to].delegateCallAllowed,
"Delegate call not allowed to this address"
);
require(allowedTargets[to].allowed, "Target address is not allowed");
if (value > 0) {
require(
allowedTargets[to].valueAllowed,
"Cannot send ETH to this target"
);
}
if (data.length >= 4) {
require(
!allowedTargets[to].scoped ||
allowedTargets[to].allowedFunctions[bytes4(data)],
"Target function is not allowed"
);
} else {
require(data.length == 0, "Function signature too short");
require(
!allowedTargets[to].scoped ||
allowedTargets[to].fallbackAllowed,
"Fallback not allowed for this address"
);
}
}
This guard is already well-established and used by projects such as Immunefi, where we have audited this guard.
However, the guards can be more complex. They can also implement the checkAfterExecution
function, or check the signatures and other values provided by the interface. This makes it possible to build invariants that not only check passed arguments, but also check if the state transition after the transaction was allowed and correct.
Another good example is a Safe Guard in the Mixin protocol, which we have also audited. It accesses the aggregated signatures and recovers the signers. If there’s a specific address in the aggregated signature, and it matches the one saved in the state of the guard, then it allows you to execute the transaction after a certain timelock.
This approach can be critically helpful when managing huge portfolios in a multisig. Transaction delay with monitoring infrastructure help react to potential security incidents. But more importantly, the discussed target scoping can completely prevent unwanted execution.
Case studies
“I’m not sure if I want to give up flexibility, but I certainly don’t ever call delegatecalls with the multisig.”
Start using a guard that prevents delegate calls. Make sure the guard is audited.
“I have a list of addresses I need to call, otherwise, I don’t need to call any others.”
Start using ScopeGuard. Be aware that this guard is permissioned, so take appropriate security measures on the guard as well (a malicious guard implementation can block Safe transactions). When the parameters in the guard are settled, in some cases it is possible to renounce ownership of the guard to prevent any changes in the guard’s behavior.
“I have special requirements, such as different multisig thresholds for different actions or invariant checking.”
Implement your own Safe Guard and get it audited.
Summary
Relying solely on off-chain security practices is not enough. Embedding protective constraints directly into blockchain protocols can offer a far more robust defense against sophisticated attacks.
Safe’s modular and flexible architecture is intentional by placing responsibility on integrators to configure it securely, allowing only the operations that are absolutely necessary. By following the principle of least privilege and minimizing unnecessary functionality, projects can significantly reduce their attack surface and improve overall security.
This particular exploit could have been prevented had the available security features been properly understood and configured to match the specific needs of the project. While Safe Guards are a powerful native solution, they aren’t the only one. Safe Modules offer even greater control and customization – and with that greater power comes greater complexity.