π― 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
- π§βπ» Saurav Kumar
- π¬ Twitter/X: @_SauravCodes
- πΌ LinkedIn: Saurav Kumar
Top comments (0)