OverviewHistoryStatsSecurity
npx skills add ...
Documentation
SKILL.md
npx skills add 0xlayerghost/solidity-agent-kit --skill solidity-deploy
[AUTO-INVOKE] MUST be invoked BEFORE deploying contracts or writing deployment scripts (*.s.sol). Covers pre-flight checks, forge script commands, post-deployment validation, and verification. Trigger: any task involving forge script, contract deployment, or block explorer verification.
npx skills add 0xlayerghost/solidity-agent-kit --skill solidity-deploy
| Step | Command / Action |
|---|---|
| Format code | forge fmt |
| Run all tests | forge test — zero failures required |
| Check gas report | forge test --gas-report — review critical functions |
| Verify config | Manually check config/*.json parameters |
| Dry-run | forge script <Script> --fork-url <RPC_URL> -vvvv (no --broadcast) |
| Check balance | cast balance <DEPLOYER> --rpc-url <RPC_URL> — sufficient gas? |
| Gas limit set | Deployment command must include --gas-limit |
| Situation | Rule |
|---|---|
| Default deployment | No --verify — contracts are not verified on block explorers by default |
| User requests verification | Add --verify and --etherscan-api-key to the command |
| Post-deploy verification | Use forge verify-contract as a separate step |
| Multi-chain deploy | Separate scripts per chain, never batch multiple chains in one script |
| Proxy deployment | Deploy implementation first, then proxy — verify both separately |
| Upgradeable contract | Use OpenZeppelin Upgrades Plugin (see below) — never hand-roll proxy deployment |
config/*.json and deployments/latest.envcast call to verify on-chain state is correctdocs/CHANGELOG.mdforge verify-contract separately--account <KEYSTORE_NAME>.PRIVATE_KEY in a gitignored .env, run source .env, then use --private-key "$PRIVATE_KEY"..env.--broadcast in templates. The user must explicitly add it when ready to deploy.For any upgradeable contract (UUPS, Transparent, Beacon), use the OpenZeppelin Foundry Upgrades Plugin instead of hand-rolling proxy deployment scripts.
| Manual Approach | With Plugin |
|---|---|
| ~30 lines: deploy impl → deploy proxy → encode initializer → wire up | 1 line: Upgrades.deployUUPSProxy(...) |
| ~20 lines: deploy new impl → validate storage → upgrade proxy | 1 line: Upgrades.upgradeProxy(...) |
| Storage layout compatibility: check by eye | Auto-checked, incompatible layouts are rejected |
Forgot _disableInitializers()? No warning | Auto-validated |
Add to remappings.txt:
Add @custom:oz-upgrades-from MyContract annotation to V2 contract for automatic reference:
| Function | Purpose |
|---|---|
Upgrades.deployUUPSProxy(contract, data) | Deploy UUPS proxy + impl + initialize |
Upgrades.deployTransparentProxy(contract, admin, data) | Deploy Transparent proxy + impl + initialize |
Upgrades.upgradeProxy(proxy, newContract, data) | Validate + deploy new impl + upgrade |
Upgrades.validateUpgrade(contract, opts) | Validate only, no deploy (for CI/tests) |
Upgrades.getImplementationAddress(proxy) | Get current implementation address |
Upgrades.prepareUpgrade(contract, opts) | Validate + deploy new impl, return address (for multisig) |
--ffi flag — the plugin needs it for storage layout validation--sender <ADDRESS> for upgrades — must match proxy owner, otherwise OwnableUnauthorizedAccountUpgrades in scripts, UnsafeUpgrades only in tests — UnsafeUpgrades skips all safety checks@custom:oz-upgrades-from annotationforge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/// script/Deploy.s.sol
import {Script, console} from "forge-std/Script.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {MyContract} from "../src/MyContract.sol";
contract DeployScript is Script {
function run() public {
vm.startBroadcast();
// One line: deploys impl + proxy + calls initialize
address proxy = Upgrades.deployUUPSProxy(
"MyContract.sol",
abi.encodeCall(MyContract.initialize, (msg.sender))
);
console.log("Proxy:", proxy);
console.log("Impl:", Upgrades.getImplementationAddress(proxy));
vm.stopBroadcast();
}
}// script/Upgrade.s.sol
import {Script, console} from "forge-std/Script.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
contract UpgradeScript is Script {
function run() public {
address proxy = vm.envAddress("PROXY_ADDRESS");
vm.startBroadcast();
// One line: validates storage layout + deploys new impl + upgrades proxy
Upgrades.upgradeProxy(proxy, "MyContractV2.sol", "");
console.log("Upgraded. New impl:", Upgrades.getImplementationAddress(proxy));
vm.stopBroadcast();
}
}/// @custom:oz-upgrades-from MyContract
contract MyContractV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
// ...
}# Deploy proxy (dry-run) — --ffi is required for storage layout checks
forge script script/Deploy.s.sol --rpc-url <RPC_URL> --ffi -vvvv
# Deploy proxy (broadcast) with Foundry Keystore
forge script script/Deploy.s.sol --rpc-url <RPC_URL> --ffi --account <KEYSTORE_NAME> --broadcast
# Or deploy with PRIVATE_KEY from a gitignored .env
source .env
forge script script/Deploy.s.sol --rpc-url <RPC_URL> --ffi --private-key "$PRIVATE_KEY" --broadcast
# Upgrade proxy (dry-run)
PROXY_ADDRESS=0x... forge script script/Upgrade.s.sol --rpc-url <RPC_URL> --ffi -vvvv
# Upgrade proxy (broadcast) with Foundry Keystore
PROXY_ADDRESS=0x... forge script script/Upgrade.s.sol --rpc-url <RPC_URL> --ffi --account <KEYSTORE_NAME> --broadcast
# Or upgrade with PRIVATE_KEY from a gitignored .env
source .env
PROXY_ADDRESS=0x... forge script script/Upgrade.s.sol --rpc-url <RPC_URL> --ffi --private-key "$PRIVATE_KEY" --broadcast
# Validate upgrade without deploying (useful for CI)
# Use Upgrades.validateUpgrade("MyContractV2.sol", opts) in a test