From atum-stack-backend
Solidity smart contract development pattern library — modern Solidity 0.8.x (built-in overflow checks, custom errors with Error(), events for off-chain indexing), Foundry toolchain (forge build/test/script with cheatcodes, fuzz tests, invariant tests, anvil local node, cast for chain interaction), Hardhat alternative (TypeScript scripting, plugin ecosystem), OpenZeppelin Contracts library (ERC-20, ERC-721, ERC-1155, AccessControl, ReentrancyGuard, Ownable, UUPS upgradeable proxies, Governor for DAOs), upgrade patterns (Transparent Proxy, UUPS, Beacon, Diamond EIP-2535), gas optimization techniques (storage packing, calldata vs memory, immutable vs constant, unchecked blocks for safe arithmetic), event-driven architecture (indexed parameters, The Graph subgraphs), and the canonical EVM contract patterns (CEI Checks-Effects-Interactions, Pull-over-Push payments, Withdrawal pattern). Use when writing Solidity contracts, designing token standards, implementing access control, auditing for gas optimization, or scaffolding a Foundry/Hardhat project. Differentiates from generic backend-patterns by EVM-specific constraints (immutable bytecode after deploy, gas costs per opcode, storage layout matters for upgrades).
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-stack-backendThis skill uses the workspace's default tool permissions.
Ce skill couvre les patterns canoniques pour écrire des smart contracts Solidity production-grade. Solidity 0.8.x est la baseline en 2026 — les versions plus anciennes ont des pièges (pas d'overflow check natif).
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides implementation of event-driven hooks in Claude Code plugins using prompt-based validation and bash commands for PreToolUse, Stop, and session events.
Ce skill couvre les patterns canoniques pour écrire des smart contracts Solidity production-grade. Solidity 0.8.x est la baseline en 2026 — les versions plus anciennes ont des pièges (pas d'overflow check natif).
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init my-project
cd my-project
forge install OpenZeppelin/openzeppelin-contracts
foundry.toml :
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.26"
optimizer = true
optimizer_runs = 200
via_ir = true
fuzz_runs = 1000
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"
forge build
forge test -vvv
forge script script/Deploy.s.sol --rpc-url sepolia --broadcast --verify
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract MyToken is ERC20, Ownable2Step, ReentrancyGuard {
// === Custom errors (gas-cheap vs require strings) ===
error ZeroAddress();
error InsufficientBalance(uint256 requested, uint256 available);
error TransferFailed();
// === Events ===
event TokensWithdrawn(address indexed user, uint256 amount);
// === Immutable state (gas-cheap, set in constructor) ===
uint256 public immutable maxSupply;
// === Storage ===
mapping(address account => uint256 lastClaim) public lastClaims;
constructor(uint256 _maxSupply, address _owner)
ERC20("MyToken", "MTK")
Ownable(_owner)
{
if (_owner == address(0)) revert ZeroAddress();
maxSupply = _maxSupply;
}
function mint(address to, uint256 amount) external onlyOwner {
if (totalSupply() + amount > maxSupply) {
revert InsufficientBalance({requested: amount, available: maxSupply - totalSupply()});
}
_mint(to, amount);
}
// === Withdrawal pattern ===
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
if (amount == 0) revert InsufficientBalance(0, 0);
// CEI: Checks-Effects-Interactions
pendingWithdrawals[msg.sender] = 0;
emit TokensWithdrawn(msg.sender, amount);
(bool success, ) = msg.sender.call{value: amount}("");
if (!success) revert TransferFailed();
}
mapping(address => uint256) public pendingWithdrawals;
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Royalty} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MyCollection is ERC721, ERC721Royalty, Ownable {
uint256 public constant MAX_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.01 ether;
uint256 public nextTokenId = 1;
string private _baseTokenURI;
error MaxSupplyReached();
error InsufficientPayment();
constructor(string memory baseURI, address royaltyReceiver, uint96 royaltyBps)
ERC721("MyCollection", "MYC")
Ownable(msg.sender)
{
_baseTokenURI = baseURI;
_setDefaultRoyalty(royaltyReceiver, royaltyBps); // EIP-2981
}
function mint() external payable {
if (nextTokenId > MAX_SUPPLY) revert MaxSupplyReached();
if (msg.value < MINT_PRICE) revert InsufficientPayment();
uint256 tokenId = nextTokenId++;
_safeMint(msg.sender, tokenId);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
function setBaseURI(string calldata baseURI) external onlyOwner {
_baseTokenURI = baseURI;
}
function withdraw() external onlyOwner {
(bool ok, ) = owner().call{value: address(this).balance}("");
require(ok, "Withdraw failed");
}
function supportsInterface(bytes4 interfaceId)
public view override(ERC721, ERC721Royalty)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyContractV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 public counter;
function initialize(address _owner) public initializer {
__Ownable_init(_owner);
__UUPSUpgradeable_init();
}
function increment() external {
counter += 1;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
Déploiement avec forge script + OpenZeppelin Upgrades :
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
address proxy = Upgrades.deployUUPSProxy(
"MyContractV1.sol",
abi.encodeCall(MyContractV1.initialize, (owner))
);
Important : pour les upgrades, le storage layout est critique. Toujours vérifier avec forge inspect et OZ Upgrades plugin.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Test, console2} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address owner = makeAddr("owner");
address alice = makeAddr("alice");
function setUp() public {
token = new MyToken(1_000_000 ether, owner);
}
function test_Mint() public {
vm.prank(owner);
token.mint(alice, 100 ether);
assertEq(token.balanceOf(alice), 100 ether);
}
function test_RevertWhen_NonOwnerMints() public {
vm.expectRevert();
vm.prank(alice);
token.mint(alice, 100 ether);
}
function test_RevertWhen_ExceedsMaxSupply() public {
vm.prank(owner);
vm.expectRevert(abi.encodeWithSelector(
MyToken.InsufficientBalance.selector, 2_000_000 ether, 1_000_000 ether
));
token.mint(alice, 2_000_000 ether);
}
}
function testFuzz_Mint(address to, uint96 amount) public {
vm.assume(to != address(0));
vm.assume(amount > 0 && amount <= 1_000_000 ether);
vm.prank(owner);
token.mint(to, amount);
assertEq(token.balanceOf(to), amount);
}
contract MyTokenInvariants is Test {
MyToken token;
function setUp() public {
token = new MyToken(1_000_000 ether, address(this));
}
function invariant_TotalSupplyNeverExceedsMax() public view {
assertLe(token.totalSupply(), token.maxSupply());
}
}
// BAD: 3 storage slots
struct UserBad {
uint256 balance; // slot 0
uint8 level; // slot 1 (gaspille 31 bytes)
address account; // slot 2
}
// GOOD: 2 storage slots
struct UserGood {
address account; // slot 0 (20 bytes)
uint8 level; // slot 0 (1 byte, packé)
uint96 balance; // slot 0 (12 bytes, packé)
// total: 33 bytes → ne tient pas, donc balance va sur slot 1
}
// Better:
struct UserBest {
uint128 balance; // slot 0 (16 bytes)
address account; // slot 0 (20 bytes) → trop, donc account va sur slot 1
uint8 level; // slot 1 (avec account)
}
uint256 public constant FEE_BPS = 100; // gravé dans le bytecode
address public immutable owner; // assigné au constructor, lecture cheap
uint256 public storageCounter; // SLOAD coûteux
// BAD: copy memory inutile
function processBad(uint256[] memory data) external { /* ... */ }
// GOOD: read directly from calldata
function processGood(uint256[] calldata data) external { /* ... */ }
function safeIncrement(uint256[] memory arr) public pure {
uint256 len = arr.length;
for (uint256 i; i < len;) {
arr[i] += 1;
unchecked { ++i; } // safe: i ne peut pas overflow car borné par len
}
}
// BAD: ~50 gas par char + storage du string
require(amount > 0, "Amount must be positive");
// GOOD: ~50 gas total
error AmountMustBePositive();
if (amount == 0) revert AmountMustBePositive();
event Transfer(address indexed from, address indexed to, uint256 amount);
event UserCreated(address indexed user, uint256 timestamp, string metadata);
indexed permet de filter sur cette propriété dans The Graph et les RPC eth_getLogs.
Sans events, la seule façon de query l'historique est de re-scanner toute la chaîne — impraticable. Les events sont la façon canonique de communiquer avec le off-chain.
nonReentrant sur les fonctions qui font des call externes → reentrancy attacks (The DAO, Cream Finance, etc.)transfer() ou send() → 2300 gas stipend insuffisant pour les nouveaux contracts. Utiliser call{value: x}("") avec check de retourtx.origin au lieu de msg.sender → phishing facileOwnable2Step → erreur de transfert irrécupérableunchecked dans les loops bornés → 30 gas perdu par itérationrequire(success) sans message → debug impossible_ prefix pour l'internal → confusion lecturescall{gas: 2300} → casse à la prochaine hard forkexternal payable ont nonReentrant ou justifient leur absenceimmutable / constant partout où c'est possibleforge snapshot --diff)smart-contract-security (ce plugin)web3-integration (ce plugin)blockchain-expert (ce plugin)