# Web2: The Hidden Layer of DeFi Risk

By [Guardian](https://paragraph.com/@guardian) · 2025-05-18

---

Web2 Vulnerabilities, Web3 Risk
-------------------------------

Web2 and Web3 security are often treated as separate domains. Labels like “Web2 Security Researcher” and “Web3 Security Researcher” create a false dichotomy — when in reality, modern DApps are deeply integrated across both layers.

Smart contracts may be secure in isolation, but DeFi’s composability introduces new, dangerous risks. **Whoever controls the calldata controls the world.** We’ve seen [billion-dollar exploits](https://rekt.news/bybit-rekt) stem from frontend misconfigurations or compromised libraries that injected malicious calldata into trusted contracts.

Why Web2 Security Matters More Than Ever
----------------------------------------

When people think about securing a DApp, they often stop at smart contracts. But the reality is that much of the DApp’s behavior is governed by what happens _offchain_ — in backends, frontends, and CI/CD pipelines. This is the “Web2 layer” of crypto.

And it’s increasingly where attackers are looking.

Unlike smart contracts, which are immutable and reviewed line-by-line, Web2 systems are mutable, fast-moving, and rarely audited with the same scrutiny. They often hold the keys to core logic:

*   Who can call sensitive contract functions
    
*   What calldata gets passed in
    
*   When transactions are triggered
    

If an attacker compromises any of those systems — by injecting fake data into a backend, modifying frontend scripts, or abusing deployment infrastructure — they can bypass the "secure" onchain logic entirely.

The issue isn’t always that smart contracts are vulnerable. It’s that everything surrounding them can make them insecure — and most teams don’t realize it until it’s too late.

Let’s examine a real-world example Guardian uncovered, where the smart contracts were sound — but the **calldata** passed through a Web2 backend made the DApp vulnerable.

Example of a Web2 Attack
------------------------

When a user registers on this website, their data is stored in the application’s backend:

    async handler(ctx, { data }) {
        let account = await accountByExternalId(ctx, data.id);
        if (account === null) {
          const newAccount = await ctx.db.insert("accounts", {
            displayName: `${data.username}`,
            externalId: data.id,
            profileImage: data.image_url,
            preferences: {
              theme: "DEFAULT",
              settingA: false,
              settingB: false,
              showValuesInUnits: false,
            },
            socialHandle: maybeGetSocialHandle(data),
          });
          account = (await ctx.db.get(newAccount)) as Doc<"accounts">;
        }
    

A cron job runs every 30 minutes to flush tokens held in user-specific “forwarder” contracts to the protocol’s treasury:

    function batchFlushERC20(
            address[] calldata forwarderAddresses,
            address tokenContractAddress
        ) public {
            for (uint256 i = 0; i < forwarderAddresses.length; i++) {
                address forwarderAddress = forwarderAddresses[i];
                Forwarder forwarder = Forwarder(forwarderAddress);
                forwarder.flushERC20(tokenContractAddress);
            }
        }
    

From a Web3 security perspective, this function looks secure, and would most likely pass a review. A web3 security researcher might check edge cases like zero addresses or reentrancy — but that’s not where the real risk lies.

Here’s how the Web2 backend calls this function:

    export const flushExternalDeposit = internalAction({
      args: {},
      handler: async (ctx) => {
        const allUserIds = await ctx.runQuery(internal.deposits.allUserIds);
        const forwarderAddresses: string[] = [];
        for (const userId of allUserIds) {
          const address = getPredeployForUser(userId, BASE_CHAIN_ID);
          if (address !== "test") {
            forwarderAddresses.push(address);
          }
        }
    
        const queueId = await contractWrite({
          contractAddress: TREASURIES[BASE_CHAIN_ID],
          functionName:
            "function batchFlushERC20(address[] forwarderAddresses, address tokenContractAddress)",
          args: [forwarderAddresses, BASE_USDC],
          abi: [
            {
              type: "function",
              name: "batchFlushERC20",
              inputs: [
                { type: "address[]", name: "forwarderAddresses" },
                { type: "address", name: "tokenContractAddress" },
              ],
              outputs: [],
            },
          ],
          chainId: BASE_CHAIN_ID,
        });
      },
    });
    

Every 30 minutes, the application fetches **all** users who have ever registered and passes their addresses to the `batchFlushERC20` function as `forwarderAddresses`.

This creates a unique problem: blockchains have a **block gas limit**. As more users register, the calldata grows — and so does the gas required to process each flush. Every additional user adds another external call to a forwarder contract.

Eventually, as the user base grows — or if a malicious actor spams thousands of fake registrations — the function call will exceed the block gas limit. At that point, the flushing mechanism fails entirely, **denying service to a critical part of the DApp’s asset flow**.

Conclusion
----------

As DApps evolve, core logic is increasingly pushed off-chain — and attackers know it. Smart contract audits alone are no longer enough.

Guardian provides **full-stack security** — auditing not just your contracts, but everything connected to them. If you’re serious about shipping a secure product, we’ll help you find and fix protocol-killing issues at every layer.

Book an audit with Guardian [here](https://guardianaudits.com/), to secure every layer of your stack.

---

*Originally published on [Guardian](https://paragraph.com/@guardian/web2-the-hidden-layer-of-defi-risk)*
