When we added package imports to Doodledapp, we hit a problem that sounds simple but has real consequences. Users need libraries like OpenZeppelin to build real smart contracts. But installing npm packages means running code from the internet on a shared build server, and in web3, that code is a prime target for attackers.
Why package imports are a security problem
Doodledapp compiles your visual flow into Solidity code, then compiles that Solidity into deployable bytecode. For basic contracts, this works with just a compiler. But the moment you want to import an external library, like OpenZeppelin’s ERC-20 implementation or Solmate’s gas-optimized contracts, the build process needs to download and install packages from npm.
That is where things get dangerous. npm packages can execute arbitrary scripts during installation. A single compromised package can read environment variables, exfiltrate data, or modify other files on the system. This is not theoretical.
In December 2024, attackers compromised the Solana web3.js library (350,000+ weekly downloads) and injected code that stole private keys, draining $160,000 in user funds. In April 2025, the XRP Ledger’s xrpl.js package was compromised with a severity score of 9.3, capturing secret key material from every application that installed it.
In September 2025, 18 widely used npm packages with billions of collective weekly downloads were hit with a crypto-draining payload that was live for hours before anyone noticed. The attack vector is always the same: a compromised maintainer account publishes a malicious version, and every project that installs it becomes a victim.
We needed to let users import any Solidity package they want while treating every package as potentially hostile.
What we considered
We evaluated four approaches before landing on the one we shipped.
| Approach | Upside | Downside | Outcome |
|---|---|---|---|
| Browser-only compilation | No server risk at all | Cannot install npm packages or resolve dependencies | Ruled out |
| Direct server compilation | Fast and simple to build | One bad package could affect the entire server | Ruled out |
| Full virtual machine per build | Maximum isolation | Too slow for real-time interactive builds | Ruled out |
| Two-stage containerized pipeline | Strong isolation with acceptable speed | More complex to build and maintain | Chose this |
Browser-only compilation is how some tools handle Solidity. It works for basic imports by fetching individual files from a CDN, but it cannot support the full npm ecosystem with transitive dependency resolution. Direct server compilation is how most developer tools work, including popular Solidity frameworks that trust npm completely and run everything on the developer’s local machine. For a shared build server where many users compile contracts, that trust model breaks down.
Full virtual machines would give us the strongest possible isolation, but spinning up a VM for every build adds seconds of latency. When you are iterating on a contract and pressing Build every few minutes, that delay kills the workflow.
The containerized approach hit the right balance: real isolation at the operating system level, fast startup, and the ability to enforce strict resource limits on memory, CPU, and process count.
How we built it
The key insight was splitting the build into two stages with different trust levels.
Stage one handles package installation. This stage needs network access to download packages from npm. It runs inside an isolated container with strict resource limits. Even if a malicious package attempts to do something harmful, it is trapped inside a container with no access to the host system, other users’ data, or any sensitive configuration.
We also disable package lifecycle scripts during installation, which removes the most common attack vector for npm supply chain compromises.
Stage two handles compilation. Once packages are installed, the Solidity compilation runs in a separate container with no network access whatsoever. The contract source code and installed packages are mounted as read-only volumes. The compiler cannot reach the internet, cannot write to the package directory, and cannot spawn processes beyond a tight limit.
If malicious code somehow made it past stage one, it would have no channel to exfiltrate data or communicate externally during compilation.
This two-stage split was not just a security decision. It turned out to be an architecture win. Because installation and compilation have different risk profiles, we could tune the resource limits, timeouts, and permissions for each stage independently.
The other critical piece was caching. We generate a deterministic key based on the exact set of packages a user needs. If that combination has been installed before, we skip the installation stage entirely and go straight to compilation with the cached result. The second time you build a contract with the same imports, the build is noticeably faster because it jumps directly to the compile step.
From your perspective, none of this is visible. You add a package in the Packages panel, browse available contracts, select what you need, and hit Build. The sandboxing, caching, and isolation happen behind the scenes.
What surprised us
The caching layer ended up being more important than we expected, and for a reason we did not anticipate.
We originally built caching purely for performance. Installing a full package like OpenZeppelin takes time, and doing it on every single build was too slow for iterative development. But caching also strengthened the security model.
Cached packages are stored outside the build container and mounted as read-only, meaning even the installation container cannot modify previously cached packages. A malicious package version published today cannot retroactively affect builds that reference a previously cached, clean version.
We also underestimated how much engineering goes into cache lifecycle management. Keeping a cache forever is not feasible with finite disk space. We built automatic cleanup that removes packages nobody has used recently while keeping frequently used combinations warm. Tuning that eviction logic took more iteration than the sandboxing itself.
What we learned
Five takeaways that apply to anyone building a tool where users trigger server-side builds.
Separate your trust boundaries. Package installation and compilation have fundamentally different risk profiles. Installation needs network access. Compilation does not. Running them in the same environment means the higher-risk operation exposes the lower-risk one. Splitting them into separate stages with separate permissions was the single most impactful decision we made.
Default to deny, then selectively allow. Our compilation containers start with no network access, no special system permissions, no write access to source or dependency files, and a cap on how many processes can run. We only grant permissions where there is a clear, specific need. This is the opposite of how most build tools work, where everything is permitted by default and restrictions are added after an incident.
Performance is a security feature. If the safe path is noticeably slower than the unsafe path, people will find workarounds. Caching makes sandboxed builds fast enough that the isolation overhead is invisible to users. If we had shipped sandboxing without caching, the performance cost might have pushed us toward compromises we did not want to make.
Lifecycle management is harder than isolation. Building a container that runs code in isolation is well-documented. Managing a cache that stays fast, stays current, and does not consume all your disk space is where the real engineering complexity lives. Plan for it early.
npm’s trust model is broken for shared environments. Every major Solidity development tool trusts npm implicitly. That model works when the developer is the only person affected by a bad package on their own machine. It breaks the moment multiple users share a build server. If you are building any tool where users trigger builds, assume every external dependency is hostile until proven otherwise.
What this means for you
You can import OpenZeppelin, Solmate, or any other Solidity library published on npm directly from the Packages panel in Doodledapp. Browse the available contracts, select the ones your project needs, configure constructor arguments if applicable, and build. Every compilation runs in an isolated environment with no access to the host server, other users, or anything outside the build itself.
Builds stay fast because of aggressive package caching. The first time you use a new package combination, there is a brief installation step. Every subsequent build with the same packages skips that step and goes straight to compilation.
We are continuing to tighten the isolation model and improve cache efficiency. Package imports are one of those features where doing it right takes more engineering than doing it fast. We would rather get the security model right than ship something that looks simple but cuts corners where it counts.