🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall`

🎯 Introduction

In modern Web3 development, extensibility and modularity are essential for scalable dApps and on-chain games. Imagine a Web3 game where players have profiles, and can install plugins like Achievements, Inventory, or Battle Sta…


This content originally appeared on DEV Community and was authored by Saurav Kumar

🎯 Introduction

In modern Web3 development, extensibility and modularity are essential for scalable dApps and on-chain games. Imagine a Web3 game where players have profiles, and can install plugins like Achievements, Inventory, or Battle Stats β€” without ever redeploying the core contract.

That’s what we’ll build today β€” a modular player profile system powered by delegatecall, where:

  • The core contract stores player data.
  • Plugins are separate contracts that extend functionality.
  • The core uses delegatecall to execute plugin logic inside its own storage context.

This real-world pattern is inspired by Lens Protocol, Decentraland, and The Sandbox, where modular smart contract architecture enables long-term upgradability and community-driven innovation.

πŸ“ Project File Structure

Here’s how your project will look inside the day16-modular-profile directory:

day16-modular-profile/
β”œβ”€β”€ foundry.toml
β”œβ”€β”€ lib/
β”‚   └── forge-std/                 # Foundry standard library
β”œβ”€β”€ script/
β”‚   └── Deploy.s.sol               # Deployment script
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ CoreProfile.sol            # Main contract
β”‚   β”œβ”€β”€ IPlugin.sol                # Plugin interface
β”‚   └── plugins/
β”‚       └── AchievementPlugin.sol  # Example plugin contract
β”œβ”€β”€ test/
β”‚   └── CoreProfile.t.sol          # (Optional) Foundry tests
└── frontend/                      # React + Ethers.js frontend (optional)
    β”œβ”€β”€ package.json
    β”œβ”€β”€ src/
    β”‚   └── App.jsx
    └── public/
        └── index.html

This mirrors real-world monorepo structures β€” smart contracts managed with Foundry, and a React frontend for interaction.

βš™οΈ Setup β€” Foundry Project

# Create new foundry project
forge init day16-modular-profile

cd day16-modular-profile

# Create plugin and script folders
mkdir -p src/plugins script

πŸ’‘ Full Source Code with Running State

1️⃣ src/IPlugin.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IPlugin {
    function execute(bytes calldata data) external;
}

2️⃣ src/CoreProfile.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IPlugin.sol";

contract CoreProfile {
    struct Profile {
        string name;
        string avatar;
        mapping(address => bool) activePlugins;
    }

    mapping(address => Profile) private profiles;
    mapping(string => address) public pluginRegistry;

    event ProfileCreated(address indexed user, string name, string avatar);
    event PluginRegistered(string name, address plugin);
    event PluginActivated(address indexed user, string plugin);
    event PluginExecuted(address indexed user, string plugin, bytes data);

    modifier onlyRegisteredPlugin(string memory _plugin) {
        require(pluginRegistry[_plugin] != address(0), "Plugin not registered");
        _;
    }

    /// @notice Create player profile
    function createProfile(string calldata _name, string calldata _avatar) external {
        Profile storage p = profiles[msg.sender];
        p.name = _name;
        p.avatar = _avatar;
        emit ProfileCreated(msg.sender, _name, _avatar);
    }

    /// @notice Register new plugin
    function registerPlugin(string calldata _name, address _plugin) external {
        pluginRegistry[_name] = _plugin;
        emit PluginRegistered(_name, _plugin);
    }

    /// @notice Activate plugin for user
    function activatePlugin(string calldata _pluginName) external onlyRegisteredPlugin(_pluginName) {
        Profile storage p = profiles[msg.sender];
        p.activePlugins[pluginRegistry[_pluginName]] = true;
        emit PluginActivated(msg.sender, _pluginName);
    }

    /// @notice Execute plugin logic via delegatecall
    function executePlugin(string calldata _pluginName, bytes calldata data)
        external
        onlyRegisteredPlugin(_pluginName)
    {
        address pluginAddr = pluginRegistry[_pluginName];
        Profile storage p = profiles[msg.sender];
        require(p.activePlugins[pluginAddr], "Plugin not active");

        (bool success, ) = pluginAddr.delegatecall(
            abi.encodeWithSelector(IPlugin.execute.selector, data)
        );
        require(success, "Delegatecall failed");

        emit PluginExecuted(msg.sender, _pluginName, data);
    }

    /// @notice Get player profile info
    function getProfile(address _user) external view returns (string memory, string memory) {
        Profile storage p = profiles[_user];
        return (p.name, p.avatar);
    }
}

3️⃣ src/plugins/AchievementPlugin.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "../IPlugin.sol";

contract AchievementPlugin is IPlugin {
    mapping(address => uint256) public achievements;

    event AchievementUnlocked(address indexed player, string achievement);

    function execute(bytes calldata data) external override {
        (string memory achievement) = abi.decode(data, (string));
        achievements[msg.sender] += 1;
        emit AchievementUnlocked(msg.sender, achievement);
    }
}

4️⃣ script/Deploy.s.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/CoreProfile.sol";
import "../src/plugins/AchievementPlugin.sol";

contract Deploy is Script {
    function run() external {
        vm.startBroadcast();

        CoreProfile core = new CoreProfile();
        AchievementPlugin plugin = new AchievementPlugin();

        core.registerPlugin("Achievements", address(plugin));

        vm.stopBroadcast();
    }
}

πŸ§ͺ Run & Test the Project

# Compile contracts
forge build

# Run deployment
forge script script/Deploy.s.sol --rpc-url <YOUR_RPC_URL> --private-key <PRIVATE_KEY> --broadcast

πŸ”’ Safe delegatecall Practices

Since delegatecall executes plugin code in the caller’s storage, safety is critical.

βœ… Follow these best practices:

  • Maintain consistent storage layout between core and plugin contracts.
  • Use a verified registry for plugin addresses.
  • Validate function selectors and return data.
  • Never let users input arbitrary contract addresses.

This design pattern resembles:

  • EIP-2535 (Diamond Standard)
  • Proxy + Logic split pattern (UUPS / Transparent Proxy)
  • Modular NFT frameworks like those used by Lens and Sandbox.

🌐 React + Ethers.js Frontend Example

Add this file under:
frontend/src/App.jsx

import { useState } from "react";
import { ethers } from "ethers";
import CoreProfileAbi from "./CoreProfile.json";

const CORE_PROFILE = "0xYourDeployedAddress";

function App() {
  const [name, setName] = useState("");
  const [avatar, setAvatar] = useState("");
  const [achievement, setAchievement] = useState("");

  async function getContract() {
    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    return new ethers.Contract(CORE_PROFILE, CoreProfileAbi, signer);
  }

  async function createProfile() {
    const contract = await getContract();
    await contract.createProfile(name, avatar);
    alert("Profile Created!");
  }

  async function activatePlugin() {
    const contract = await getContract();
    await contract.activatePlugin("Achievements");
    alert("Plugin Activated!");
  }

  async function unlockAchievement() {
    const contract = await getContract();
    const data = ethers.AbiCoder.defaultAbiCoder().encode(["string"], [achievement]);
    await contract.executePlugin("Achievements", data);
    alert("Achievement Unlocked!");
  }

  return (
    <div className="p-8 max-w-lg mx-auto text-center">
      <h1 className="text-2xl font-bold mb-4">Web3 Modular Profile</h1>

      <input placeholder="Name" onChange={e => setName(e.target.value)} className="border p-2 mb-2" />
      <input placeholder="Avatar URL" onChange={e => setAvatar(e.target.value)} className="border p-2 mb-2" />
      <button onClick={createProfile} className="bg-blue-500 text-white px-4 py-2 rounded">Create Profile</button>

      <hr className="my-4" />
      <button onClick={activatePlugin} className="bg-green-500 text-white px-4 py-2 rounded">Activate Plugin</button>

      <hr className="my-4" />
      <input placeholder="Achievement" onChange={e => setAchievement(e.target.value)} className="border p-2 mb-2" />
      <button onClick={unlockAchievement} className="bg-purple-500 text-white px-4 py-2 rounded">Unlock</button>
    </div>
  );
}

export default App;

πŸš€ Real-World Applications

This modular architecture directly maps to real industry use cases:

  • GameFi Platforms β†’ Plugin-based avatars and stats (e.g., TreasureDAO)
  • Social Protocols β†’ Extensible profile features (e.g., Lens Protocol)
  • DAO Tools β†’ Modular role or reward extensions
  • Metaverse Worlds β†’ Upgradable land or character logic

🧠 Key Takeaways

  • delegatecall lets plugins run logic inside the caller’s storage.
  • Modular systems enable feature expansion without redeploying.
  • Always validate plugins to avoid malicious injections.
  • Industry-standard approach for scalable Web3 design.

πŸ”— Follow Me


This content originally appeared on DEV Community and was authored by Saurav Kumar


Print Share Comment Cite Upload Translate Updates
APA

Saurav Kumar | Sciencx (2025-10-17T18:07:12+00:00) 🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall`. Retrieved from https://www.scien.cx/2025/10/17/%f0%9f%a7%a9-day-16-of-30daysofsolidity-building-a-modular-profile-system-for-web3-games-with-delegatecall/

MLA
" » 🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall`." Saurav Kumar | Sciencx - Friday October 17, 2025, https://www.scien.cx/2025/10/17/%f0%9f%a7%a9-day-16-of-30daysofsolidity-building-a-modular-profile-system-for-web3-games-with-delegatecall/
HARVARD
Saurav Kumar | Sciencx Friday October 17, 2025 » 🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall`., viewed ,<https://www.scien.cx/2025/10/17/%f0%9f%a7%a9-day-16-of-30daysofsolidity-building-a-modular-profile-system-for-web3-games-with-delegatecall/>
VANCOUVER
Saurav Kumar | Sciencx - » 🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall`. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/17/%f0%9f%a7%a9-day-16-of-30daysofsolidity-building-a-modular-profile-system-for-web3-games-with-delegatecall/
CHICAGO
" » 🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall`." Saurav Kumar | Sciencx - Accessed . https://www.scien.cx/2025/10/17/%f0%9f%a7%a9-day-16-of-30daysofsolidity-building-a-modular-profile-system-for-web3-games-with-delegatecall/
IEEE
" » 🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall`." Saurav Kumar | Sciencx [Online]. Available: https://www.scien.cx/2025/10/17/%f0%9f%a7%a9-day-16-of-30daysofsolidity-building-a-modular-profile-system-for-web3-games-with-delegatecall/. [Accessed: ]
rf:citation
» 🧩 Day 16 of #30DaysOfSolidity β€” Building a Modular Profile System for Web3 Games with `delegatecall` | Saurav Kumar | Sciencx | https://www.scien.cx/2025/10/17/%f0%9f%a7%a9-day-16-of-30daysofsolidity-building-a-modular-profile-system-for-web3-games-with-delegatecall/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.