diff --git a/README.md b/README.md index 816a11f3..2beeaa2b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Skills are drop-in modules. No additional configuration required for basic usage | botchan | [botchan](botchan/) | Onchain messaging protocol on Base. Agent feeds, DMs, permanent data storage. | | [qrcoin](https://qrcoin.fun) | [qrcoin](qrcoin/) | QR code auction platform on Base. Programmatic bidding for URL display. | | yoink | [yoink](yoink/) | Onchain capture-the-flag on Base. | -| base | — | Planned | +| [base](https://base.org) | [base](base/) | Smart contract development on Base. Deploy contracts, manage wallets, agent-to-agent payments. | | neynar | — | Planned | | zapper | — | Planned | @@ -44,8 +44,13 @@ openclaw-skills/ │ └── scripts/ │ └── bankr.sh │ -├── base/ # Base (placeholder) -│ └── SKILL.md +├── base/ # Base +│ ├── SKILL.md +│ └── references/ +│ ├── cdp-setup.md +│ ├── deployment.md +│ ├── testing.md +│ └── ... ├── neynar/ # Neynar (placeholder) │ └── SKILL.md ├── qrcoin/ # QR Coin (community) diff --git a/base/SKILL.md b/base/SKILL.md index dab5f83e..27735f68 100644 --- a/base/SKILL.md +++ b/base/SKILL.md @@ -1,4 +1,43 @@ --- name: base -description: Placeholder for Base skill. +description: Build on Base blockchain. Use for smart contract development, deployment, wallet management, and agent-to-agent financial agreements. Routes to sub-skills for specific tasks. --- + +# Base + +Build on Base blockchain using Foundry and CDP SDK. + +**Security**: Uses CDP managed wallets. Agent never accesses private keys. + +## Sub-Skills + +| Task | Reference | Use When | +|------|-----------|----------| +| Wallet Setup | [cdp-setup.md](references/cdp-setup.md) | Creating wallets, CDP authentication | +| Testnet Faucet | [cdp-faucet.md](references/cdp-faucet.md) | Getting Base Sepolia test ETH | +| Contract Dev | [contract-development.md](references/contract-development.md) | Writing Solidity contracts | +| Testing | [testing.md](references/testing.md) | Testing with Foundry | +| Deployment | [deployment.md](references/deployment.md) | Deploying to Base | +| Verification | [verification.md](references/verification.md) | Verifying on Basescan | +| Interaction | [contract-interaction.md](references/contract-interaction.md) | Reading/writing contracts | +| Agent Patterns | [agent-patterns.md](references/agent-patterns.md) | Escrow, payments, tokens | + +## Networks + +| Network | ID | Chain ID | +|---------|-----|----------| +| Base Mainnet | `base` | 8453 | +| Base Sepolia | `base-sepolia` | 84532 | + +## Quick Reference + +```bash +# Install +curl -L https://foundry.paradigm.xyz | bash && foundryup +npm install @coinbase/cdp-sdk + +# Build & test +forge build && forge test + +# Deploy (see deployment.md) +``` diff --git a/base/references/agent-patterns.md b/base/references/agent-patterns.md new file mode 100644 index 00000000..fce13e0f --- /dev/null +++ b/base/references/agent-patterns.md @@ -0,0 +1,153 @@ +# Agent-to-Agent Patterns + +Smart contract patterns for agent-to-agent transactions. + +## When to Use + +Deploy a contract when agents need: +- Trustless payment agreements +- Automatic revenue splits +- Escrow for deliverables +- Custom tokens for agent economy + +## Escrow Contract + +Hold funds until conditions met. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract SimpleEscrow { + address public payer; + address public payee; + uint256 public amount; + bool public released; + + constructor(address _payee) payable { + payer = msg.sender; + payee = _payee; + amount = msg.value; + } + + function release() external { + require(msg.sender == payer, "Only payer"); + require(!released, "Already released"); + released = true; + payable(payee).transfer(amount); + } + + function refund() external { + require(msg.sender == payee, "Only payee"); + require(!released, "Already released"); + released = true; + payable(payer).transfer(amount); + } +} +``` + +**Deploy with value:** +```typescript +const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: undefined, + data: bytecode + encodedPayeeAddress, + value: parseEther("0.1"), + }, +}); +``` + +## Payment Splitter + +Automatic revenue distribution. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/finance/PaymentSplitter.sol"; + +contract RevenueSplit is PaymentSplitter { + constructor( + address[] memory payees, + uint256[] memory shares_ + ) PaymentSplitter(payees, shares_) {} +} +``` + +**Deploy for 60/40 split:** +```typescript +const payees = ["0xAgent1...", "0xAgent2..."]; +const shares = [60, 40]; +// Encode and deploy... +``` + +## Simple Token + +Custom ERC-20 for agent economy. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract AgentToken is ERC20 { + constructor(uint256 initialSupply) ERC20("AgentToken", "AGT") { + _mint(msg.sender, initialSupply); + } +} +``` + +## Conditional Payment + +Pay when condition is verified. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract ConditionalPayment { + address public payer; + address public payee; + address public arbiter; + uint256 public amount; + bool public completed; + + constructor(address _payee, address _arbiter) payable { + payer = msg.sender; + payee = _payee; + arbiter = _arbiter; + amount = msg.value; + } + + function approve() external { + require(msg.sender == arbiter, "Only arbiter"); + require(!completed, "Already completed"); + completed = true; + payable(payee).transfer(amount); + } + + function reject() external { + require(msg.sender == arbiter, "Only arbiter"); + require(!completed, "Already completed"); + completed = true; + payable(payer).transfer(amount); + } +} +``` + +## Workflow + +1. Agents negotiate terms +2. One agent deploys contract with terms encoded +3. Other agent verifies contract source +4. Funds deposited +5. Conditions met → funds released + +## Resources + +- [OpenZeppelin PaymentSplitter](https://docs.openzeppelin.com/contracts/4.x/api/finance#PaymentSplitter) +- [OpenZeppelin ERC20](https://docs.openzeppelin.com/contracts/4.x/erc20) diff --git a/base/references/cdp-faucet.md b/base/references/cdp-faucet.md new file mode 100644 index 00000000..ec3612bc --- /dev/null +++ b/base/references/cdp-faucet.md @@ -0,0 +1,116 @@ +# CDP Faucet - Testnet ETH + +Request testnet ETH programmatically via the CDP SDK. + +## Using CDP SDK (Recommended) + +The CDP SDK provides a simple method to request testnet funds: + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; + +const cdp = new CdpClient(); + +// Get or create your wallet +const account = await cdp.evm.getOrCreateAccount({ + name: "my-agent-wallet", +}); + +// Request testnet ETH +const faucetResp = await cdp.evm.requestFaucet({ + address: account.address, + network: "base-sepolia", + token: "eth", +}); + +console.log("Faucet transaction:", faucetResp.transactionHash); +``` + +## Supported Networks + +| Network | Network ID | Token | +|---------|------------|-------| +| Base Sepolia | `base-sepolia` | `eth` | +| Ethereum Sepolia | `ethereum-sepolia` | `eth` | + +## Complete Example + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; + +async function fundWallet() { + const cdp = new CdpClient(); + + // Create or get wallet + const account = await cdp.evm.getOrCreateAccount({ + name: "deployer", + }); + + console.log("Wallet address:", account.address); + + // Check current balance + const balance = await cdp.evm.getBalance({ + address: account.address, + network: "base-sepolia", + }); + + console.log("Current balance:", balance.amount); + + // Request faucet if balance is low + if (BigInt(balance.amount) < BigInt("100000000000000000")) { // 0.1 ETH + console.log("Requesting faucet..."); + + const faucetResp = await cdp.evm.requestFaucet({ + address: account.address, + network: "base-sepolia", + token: "eth", + }); + + console.log("Faucet tx:", faucetResp.transactionHash); + console.log("View:", `https://sepolia.basescan.org/tx/${faucetResp.transactionHash}`); + + // Wait for confirmation + await new Promise(r => setTimeout(r, 5000)); + + // Check new balance + const newBalance = await cdp.evm.getBalance({ + address: account.address, + network: "base-sepolia", + }); + + console.log("New balance:", newBalance.amount); + } else { + console.log("Sufficient balance, no faucet needed"); + } +} + +fundWallet().catch(console.error); +``` + +## Rate Limits + +The CDP faucet has rate limits to prevent abuse: +- Limited requests per address per day +- If rate limited, wait before retrying + +## Alternative Faucets + +If CDP faucet is unavailable or rate limited: + +- **Alchemy Faucet**: https://www.alchemy.com/faucets/base-sepolia +- **QuickNode Faucet**: https://faucet.quicknode.com/base/sepolia +- **Chainlink Faucet**: https://faucets.chain.link/base-sepolia + +## Troubleshooting + +**"Rate limited"** +- Wait before retrying +- Use alternative faucets listed above + +**"Network not supported"** +- Verify network is exactly `base-sepolia` +- Faucet only works for testnets + +**"Authentication error"** +- Check CDP_API_KEY_ID and CDP_API_KEY_SECRET +- Ensure environment variables are set correctly diff --git a/base/references/cdp-setup.md b/base/references/cdp-setup.md new file mode 100644 index 00000000..a4e5fc88 --- /dev/null +++ b/base/references/cdp-setup.md @@ -0,0 +1,208 @@ +# CDP SDK Setup + +Set up the Coinbase Developer Platform SDK for secure wallet management. The agent never accesses private keys directly. + +## Why CDP for Agents? + +| Raw Private Key | CDP Credentials | +|-----------------|-----------------| +| Full wallet control | Limited API access | +| Cannot be revoked | Revoke instantly if compromised | +| If leaked, funds lost | If leaked, revoke and create new | +| Agent can share accidentally | Mitigatable even if shared | + +## Installation + +```bash +npm install @coinbase/cdp-sdk +``` + +## Get API Credentials + +1. Go to [Coinbase Developer Platform](https://portal.cdp.coinbase.com/) +2. Sign in or create an account +3. Create a new project +4. Navigate to **API Keys** > **Secret API Keys** +5. Click **Create API key** +6. Choose **Ed25519** signature algorithm (recommended) +7. Save the credentials securely + +## Environment Variables + +Create a `.env` file (add to `.gitignore`): + +```bash +# CDP API credentials +CDP_API_KEY_ID=your_api_key_id +CDP_API_KEY_SECRET="-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIBkg...your_key_content_here... +-----END EC PRIVATE KEY-----" +CDP_WALLET_SECRET=your_wallet_secret + +# Optional: for contract verification +BASESCAN_API_KEY=your_basescan_api_key +``` + +**Important**: Use actual newlines in the secret, not `\n` characters. + +## Initialize the Client + +### From Environment Variables (Recommended) + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; + +// Automatically reads from environment +const cdp = new CdpClient(); +``` + +### Explicit Configuration + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; +import { readFileSync } from "fs"; + +const cdp = new CdpClient({ + apiKeyId: process.env.CDP_API_KEY_ID, + apiKeySecret: process.env.CDP_API_KEY_SECRET, + walletSecret: process.env.CDP_WALLET_SECRET, +}); +``` + +## Create a Managed Wallet + +```typescript +// Create a new EVM account +const account = await cdp.evm.createAccount(); +console.log("Address:", account.address); + +// Or get/create by name (idempotent - same name returns same wallet) +const account = await cdp.evm.getOrCreateAccount({ + name: "my-agent-wallet", +}); +``` + +## Check Balance + +```typescript +const balance = await cdp.evm.getBalance({ + address: account.address, + network: "base-sepolia", // or "base" for mainnet +}); + +console.log("Balance:", balance.amount, balance.asset); +``` + +## Send a Transaction + +```typescript +import { parseEther } from "viem"; + +const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: "0x...", + value: parseEther("0.01"), + }, +}); + +console.log("Transaction:", transactionHash); +``` + +## Transfer Tokens + +```typescript +import { parseUnits } from "viem"; + +const { transactionHash } = await account.transfer({ + to: "0x...", + amount: parseUnits("10", 6), // 10 USDC (6 decimals) + token: "usdc", + network: "base-sepolia", +}); +``` + +## Import Existing Account + +If you have an existing private key (use with caution): + +```typescript +const account = await cdp.evm.importAccount({ + privateKey: "0x...", + name: "imported-wallet", +}); +``` + +## Smart Accounts (Account Abstraction) + +For gasless transactions and advanced features: + +```typescript +const owner = await cdp.evm.createAccount(); +const smartAccount = await cdp.evm.getOrCreateSmartAccount({ + name: "my-smart-wallet", + owner, +}); + +// Send user operation (can sponsor gas) +const userOp = await cdp.evm.sendUserOperation({ + smartAccount, + network: "base-sepolia", + calls: [ + { + to: "0x...", + value: parseEther("0.01"), + data: "0x", + }, + ], +}); +``` + +## Verify Setup + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; + +async function testSetup() { + try { + const cdp = new CdpClient(); + const account = await cdp.evm.createAccount(); + console.log("Setup successful!"); + console.log("Wallet address:", account.address); + } catch (error) { + console.error("Setup failed:", error.message); + } +} + +testSetup(); +``` + +## Security Best Practices + +1. **Never commit credentials** - Use `.env` and add to `.gitignore` +2. **Rotate keys regularly** - Create new keys periodically +3. **Scope permissions** - Use minimal required permissions +4. **Monitor usage** - Check CDP dashboard for unexpected activity +5. **Use named accounts** - `getOrCreateAccount` is idempotent and safer + +## Troubleshooting + +**"Invalid API key"** +- Verify CDP_API_KEY_ID is correct +- Check key hasn't been revoked in CDP portal + +**"Invalid signature"** +- Ensure API Key Secret is in correct PEM format +- Use actual newlines, not literal `\n` characters +- Verify the full key content is included + +**"Wallet secret required"** +- Set CDP_WALLET_SECRET environment variable +- This is used for wallet encryption + +## Resources + +- [CDP Portal](https://portal.cdp.coinbase.com/) +- [CDP SDK TypeScript Docs](https://coinbase.github.io/cdp-sdk/typescript) +- [CDP SDK GitHub](https://github.com/coinbase/cdp-sdk) diff --git a/base/references/contract-development.md b/base/references/contract-development.md new file mode 100644 index 00000000..f1bf0bbd --- /dev/null +++ b/base/references/contract-development.md @@ -0,0 +1,304 @@ +# Contract Development + +Write Solidity smart contracts using Foundry. + +## Install Foundry + +```bash +curl -L https://foundry.paradigm.xyz | bash +foundryup +``` + +Verify: +```bash +forge --version +``` + +## Initialize Project + +```bash +forge init my-contract +cd my-contract +``` + +## Project Structure + +``` +my-contract/ +├── src/ # Contract source files +├── test/ # Test files +├── script/ # Deployment scripts +├── lib/ # Dependencies +├── foundry.toml # Configuration +└── .env # Environment variables (gitignored) +``` + +## Simple Counter Contract + +```solidity +// src/Counter.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract Counter { + uint256 public number; + + event NumberChanged(uint256 newNumber); + + function setNumber(uint256 newNumber) public { + number = newNumber; + emit NumberChanged(newNumber); + } + + function increment() public { + number++; + emit NumberChanged(number); + } + + function decrement() public { + require(number > 0, "Cannot decrement below zero"); + number--; + emit NumberChanged(number); + } +} +``` + +## Simple Escrow Contract + +For agent-to-agent payment agreements: + +```solidity +// src/SimpleEscrow.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract SimpleEscrow { + address public payer; + address public payee; + uint256 public amount; + bool public released; + + event Deposited(address indexed payer, uint256 amount); + event Released(address indexed payee, uint256 amount); + event Refunded(address indexed payer, uint256 amount); + + constructor(address _payee) payable { + require(msg.value > 0, "Must deposit ETH"); + payer = msg.sender; + payee = _payee; + amount = msg.value; + emit Deposited(payer, amount); + } + + function release() external { + require(msg.sender == payer, "Only payer can release"); + require(!released, "Already released"); + released = true; + payable(payee).transfer(amount); + emit Released(payee, amount); + } + + function refund() external { + require(msg.sender == payee, "Only payee can refund"); + require(!released, "Already released"); + released = true; + payable(payer).transfer(amount); + emit Refunded(payer, amount); + } +} +``` + +## Using OpenZeppelin + +Install OpenZeppelin contracts: + +```bash +forge install OpenZeppelin/openzeppelin-contracts +``` + +Update `foundry.toml`: +```toml +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/" +] +``` + +### ERC-20 Token + +```solidity +// src/MyToken.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MyToken is ERC20, Ownable { + constructor( + string memory name, + string memory symbol, + uint256 initialSupply + ) ERC20(name, symbol) Ownable(msg.sender) { + _mint(msg.sender, initialSupply * 10 ** decimals()); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} +``` + +### ERC-721 NFT + +```solidity +// src/MyNFT.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MyNFT is ERC721, ERC721URIStorage, Ownable { + uint256 private _nextTokenId; + + constructor() ERC721("MyNFT", "MNFT") Ownable(msg.sender) {} + + function safeMint(address to, string memory uri) public onlyOwner { + uint256 tokenId = _nextTokenId++; + _safeMint(to, tokenId); + _setTokenURI(tokenId, uri); + } + + // Required overrides + function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) { + return super.tokenURI(tokenId); + } + + function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) { + return super.supportsInterface(interfaceId); + } +} +``` + +### Payment Splitter + +For automatic revenue distribution: + +```solidity +// src/RevenueSplit.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/finance/PaymentSplitter.sol"; + +contract RevenueSplit is PaymentSplitter { + constructor( + address[] memory payees, + uint256[] memory shares_ + ) PaymentSplitter(payees, shares_) {} +} +``` + +Usage: +```typescript +// Deploy with 60/40 split +const payees = ["0xAgent1...", "0xAgent2..."]; +const shares = [60, 40]; +``` + +## Compile Contracts + +```bash +forge build +``` + +Output is in `out/` directory. + +## Check Contract Sizes + +```bash +forge build --sizes +``` + +Contracts must be under 24KB for deployment. + +## foundry.toml Configuration + +```toml +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +optimizer = true +optimizer_runs = 200 +solc_version = "0.8.19" + +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/" +] + +[rpc_endpoints] +base_sepolia = "${BASE_SEPOLIA_RPC}" +base = "${BASE_MAINNET_RPC}" + +[etherscan] +base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" } +base = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api" } +``` + +## Common Patterns + +### Ownable (Access Control) + +```solidity +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MyContract is Ownable { + constructor() Ownable(msg.sender) {} + + function adminOnly() public onlyOwner { + // Only owner can call + } +} +``` + +### Pausable (Emergency Stop) + +```solidity +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MyContract is Pausable, Ownable { + function transfer() public whenNotPaused { + // Cannot call when paused + } + + function pause() public onlyOwner { + _pause(); + } +} +``` + +### ReentrancyGuard + +```solidity +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +contract MyContract is ReentrancyGuard { + function withdraw() public nonReentrant { + // Protected from reentrancy attacks + } +} +``` + +## Resources + +- [Foundry Book](https://book.getfoundry.sh/) +- [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts) +- [Solidity Docs](https://docs.soliditylang.org/) diff --git a/base/references/contract-interaction.md b/base/references/contract-interaction.md new file mode 100644 index 00000000..4709eeaa --- /dev/null +++ b/base/references/contract-interaction.md @@ -0,0 +1,308 @@ +# Contract Interaction + +Read from and write to deployed contracts. + +## Read Contract State (No Transaction) + +Reading is free and doesn't require signing. + +### Using cast (CLI) + +```bash +# Read a public variable +cast call 0xContractAddress "number()" --rpc-url https://sepolia.base.org + +# Read with arguments +cast call 0xContractAddress "balanceOf(address)" 0xUserAddress --rpc-url https://sepolia.base.org + +# Decode the result +cast call 0xContractAddress "number()" --rpc-url https://sepolia.base.org | cast --to-dec +``` + +### Using viem + +```typescript +import { createPublicClient, http } from "viem"; +import { baseSepolia } from "viem/chains"; + +const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), +}); + +// Read contract +const result = await publicClient.readContract({ + address: "0x...", + abi: counterAbi, + functionName: "number", +}); + +console.log("Number:", result); +``` + +## Write to Contract (Transaction Required) + +Writing requires a transaction signed by CDP. + +### Using CDP SDK + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; +import { encodeFunctionData } from "viem"; + +const cdp = new CdpClient(); +const account = await cdp.evm.getOrCreateAccount({ name: "my-wallet" }); + +// Encode the function call +const calldata = encodeFunctionData({ + abi: counterAbi, + functionName: "setNumber", + args: [42n], +}); + +// Send transaction +const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: "0xContractAddress", + data: calldata, + }, +}); + +console.log("Transaction:", transactionHash); +``` + +### With ETH Value + +For payable functions: + +```typescript +import { parseEther } from "viem"; + +const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: "0xContractAddress", + data: calldata, + value: parseEther("0.1"), + }, +}); +``` + +## Example: Counter Contract + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; +import { createPublicClient, http, encodeFunctionData } from "viem"; +import { baseSepolia } from "viem/chains"; + +const CONTRACT_ADDRESS = "0x..."; + +const counterAbi = [ + { + inputs: [], + name: "number", + outputs: [{ type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ name: "newNumber", type: "uint256" }], + name: "setNumber", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "increment", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +async function main() { + const cdp = new CdpClient(); + const account = await cdp.evm.getOrCreateAccount({ name: "my-wallet" }); + + const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), + }); + + // Read current value + const before = await publicClient.readContract({ + address: CONTRACT_ADDRESS, + abi: counterAbi, + functionName: "number", + }); + console.log("Before:", before); + + // Increment + const calldata = encodeFunctionData({ + abi: counterAbi, + functionName: "increment", + }); + + const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: CONTRACT_ADDRESS, + data: calldata, + }, + }); + + console.log("Tx:", transactionHash); + + // Wait for confirmation + await publicClient.waitForTransactionReceipt({ + hash: transactionHash as `0x${string}`, + }); + + // Read new value + const after = await publicClient.readContract({ + address: CONTRACT_ADDRESS, + abi: counterAbi, + functionName: "number", + }); + console.log("After:", after); +} + +main().catch(console.error); +``` + +## Example: Escrow Contract + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; +import { encodeFunctionData, parseEther } from "viem"; + +const escrowAbi = [ + { + inputs: [], + name: "release", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "refund", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +async function releaseEscrow(contractAddress: string) { + const cdp = new CdpClient(); + const account = await cdp.evm.getOrCreateAccount({ name: "payer" }); + + const calldata = encodeFunctionData({ + abi: escrowAbi, + functionName: "release", + }); + + const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: contractAddress, + data: calldata, + }, + }); + + console.log("Released! Tx:", transactionHash); +} +``` + +## Using cast for Writes + +For quick testing (requires private key): + +```bash +# Set number +cast send 0xContractAddress "setNumber(uint256)" 42 \ + --rpc-url https://sepolia.base.org \ + --private-key $PRIVATE_KEY + +# Increment +cast send 0xContractAddress "increment()" \ + --rpc-url https://sepolia.base.org \ + --private-key $PRIVATE_KEY +``` + +**Note**: For production, use CDP SDK instead of raw private keys. + +## Watch for Events + +```typescript +import { createPublicClient, http, parseAbiItem } from "viem"; +import { baseSepolia } from "viem/chains"; + +const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), +}); + +// Watch for NumberChanged events +const unwatch = publicClient.watchEvent({ + address: CONTRACT_ADDRESS, + event: parseAbiItem("event NumberChanged(uint256 newNumber)"), + onLogs: (logs) => { + for (const log of logs) { + console.log("Number changed to:", log.args.newNumber); + } + }, +}); + +// Stop watching +// unwatch(); +``` + +## Get Past Events + +```typescript +const logs = await publicClient.getLogs({ + address: CONTRACT_ADDRESS, + event: parseAbiItem("event NumberChanged(uint256 newNumber)"), + fromBlock: 0n, + toBlock: "latest", +}); + +for (const log of logs) { + console.log("Block:", log.blockNumber, "Value:", log.args.newNumber); +} +``` + +## Multicall (Batch Reads) + +```typescript +const results = await publicClient.multicall({ + contracts: [ + { + address: CONTRACT_ADDRESS, + abi: counterAbi, + functionName: "number", + }, + { + address: TOKEN_ADDRESS, + abi: erc20Abi, + functionName: "balanceOf", + args: [account.address], + }, + ], +}); + +console.log("Counter:", results[0].result); +console.log("Balance:", results[1].result); +``` + +## Resources + +- [viem Documentation](https://viem.sh/) +- [cast Command Reference](https://book.getfoundry.sh/reference/cast/) diff --git a/base/references/deployment.md b/base/references/deployment.md new file mode 100644 index 00000000..38c937fe --- /dev/null +++ b/base/references/deployment.md @@ -0,0 +1,307 @@ +# Contract Deployment + +Deploy contracts to Base using CDP SDK. The agent never accesses private keys. + +## Prerequisites + +1. Foundry installed (`forge --version`) +2. CDP SDK installed (`npm install @coinbase/cdp-sdk`) +3. CDP credentials configured (see [cdp-setup.md](cdp-setup.md)) +4. Contract compiled (`forge build`) + +## Compile Contract + +```bash +forge build +``` + +Output is in `out/.sol/.json`. + +## Basic Deployment + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; +import { readFileSync } from "fs"; + +async function deploy() { + const cdp = new CdpClient(); + + // Get or create wallet + const account = await cdp.evm.getOrCreateAccount({ + name: "deployer", + }); + console.log("Deployer:", account.address); + + // Read compiled bytecode + const artifact = JSON.parse( + readFileSync("out/Counter.sol/Counter.json", "utf8") + ); + + // Deploy (to: undefined = contract creation) + const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", // or "base" for mainnet + transaction: { + to: undefined, + data: artifact.bytecode.object, + }, + }); + + console.log("Tx:", transactionHash); + console.log("Explorer:", `https://sepolia.basescan.org/tx/${transactionHash}`); +} + +deploy().catch(console.error); +``` + +## Deploy with Constructor Arguments + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk"; +import { encodeAbiParameters, parseAbiParameters } from "viem"; +import { readFileSync } from "fs"; + +async function deployWithArgs() { + const cdp = new CdpClient(); + const account = await cdp.evm.getOrCreateAccount({ name: "deployer" }); + + // Read bytecode + const artifact = JSON.parse( + readFileSync("out/MyToken.sol/MyToken.json", "utf8") + ); + + // Encode constructor arguments + // constructor(string name, string symbol, uint256 initialSupply) + const constructorArgs = encodeAbiParameters( + parseAbiParameters("string, string, uint256"), + ["My Token", "MTK", 1000000n] + ); + + // Combine bytecode + constructor args + const deployData = artifact.bytecode.object + constructorArgs.slice(2); + + const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: undefined, + data: deployData, + }, + }); + + console.log("Deployed:", transactionHash); +} + +deployWithArgs().catch(console.error); +``` + +## Deploy with ETH Value + +For payable constructors (e.g., escrow): + +```typescript +import { parseEther } from "viem"; + +const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: "base-sepolia", + transaction: { + to: undefined, + data: deployData, + value: parseEther("0.1"), // Send 0.1 ETH to contract + }, +}); +``` + +## Get Contract Address + +The contract address is derived from the deployer address and nonce: + +```typescript +import { getContractAddress } from "viem"; + +// After deployment, get contract address +const contractAddress = getContractAddress({ + from: account.address, + nonce: BigInt(nonce), // Get nonce before deployment +}); +``` + +Or fetch from transaction receipt: + +```typescript +import { createPublicClient, http } from "viem"; +import { baseSepolia } from "viem/chains"; + +const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), +}); + +const receipt = await publicClient.waitForTransactionReceipt({ + hash: transactionHash, +}); + +console.log("Contract address:", receipt.contractAddress); +``` + +## Complete Deployment Script + +```typescript +// scripts/deploy.ts +import { CdpClient } from "@coinbase/cdp-sdk"; +import { createPublicClient, http } from "viem"; +import { baseSepolia, base } from "viem/chains"; +import { readFileSync } from "fs"; + +interface DeployConfig { + contractName: string; + constructorArgs?: `0x${string}`; + value?: bigint; + network: "base-sepolia" | "base"; +} + +async function deploy(config: DeployConfig) { + const cdp = new CdpClient(); + + // 1. Get wallet + const account = await cdp.evm.getOrCreateAccount({ + name: "contract-deployer", + }); + console.log("Deployer:", account.address); + + // 2. Check balance + const balance = await cdp.evm.getBalance({ + address: account.address, + network: config.network, + }); + console.log("Balance:", balance.amount); + + // 3. Request faucet if testnet and low balance + if (config.network === "base-sepolia" && BigInt(balance.amount) < BigInt("50000000000000000")) { + console.log("Requesting faucet..."); + await cdp.evm.requestFaucet({ + address: account.address, + network: "base-sepolia", + token: "eth", + }); + await new Promise(r => setTimeout(r, 5000)); + } + + // 4. Read artifact + const artifact = JSON.parse( + readFileSync(`out/${config.contractName}.sol/${config.contractName}.json`, "utf8") + ); + + // 5. Prepare deploy data + let deployData = artifact.bytecode.object; + if (config.constructorArgs) { + deployData += config.constructorArgs.slice(2); + } + + // 6. Deploy + console.log(`Deploying ${config.contractName}...`); + const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: config.network, + transaction: { + to: undefined, + data: deployData, + value: config.value, + }, + }); + + console.log("Transaction:", transactionHash); + + // 7. Wait for receipt + const chain = config.network === "base-sepolia" ? baseSepolia : base; + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash: transactionHash as `0x${string}`, + }); + + console.log("Contract deployed at:", receipt.contractAddress); + console.log("Gas used:", receipt.gasUsed.toString()); + + const explorer = config.network === "base-sepolia" + ? "https://sepolia.basescan.org" + : "https://basescan.org"; + + console.log("Explorer:", `${explorer}/address/${receipt.contractAddress}`); + + return receipt.contractAddress; +} + +// Deploy Counter +deploy({ + contractName: "Counter", + network: "base-sepolia", +}).catch(console.error); +``` + +Run: +```bash +npx ts-node scripts/deploy.ts +``` + +## Gas Estimation + +Before deploying, estimate gas: + +```bash +# Compile and check sizes +forge build --sizes + +# Get current gas price +cast gas-price --rpc-url https://sepolia.base.org +``` + +Or programmatically: + +```typescript +const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), +}); + +const gasPrice = await publicClient.getGasPrice(); +console.log("Gas price:", gasPrice); +``` + +## Networks + +| Network | Network ID | Chain ID | Explorer | +|---------|------------|----------|----------| +| Base Mainnet | `base` | 8453 | basescan.org | +| Base Sepolia | `base-sepolia` | 84532 | sepolia.basescan.org | + +## Best Practices + +1. **Always test on Sepolia first** - Never deploy untested code to mainnet +2. **Verify after deployment** - See [verification.md](verification.md) +3. **Save deployment info** - Record contract address and deployment tx +4. **Check gas costs** - Estimate before mainnet deployment +5. **Use named wallets** - `getOrCreateAccount` with consistent names + +## Troubleshooting + +**"Insufficient funds"** +- Check balance: `cdp.evm.getBalance()` +- Request faucet: `cdp.evm.requestFaucet()` (testnet only) + +**"Contract too large"** +- Enable optimizer in foundry.toml +- Split into multiple contracts +- Max size is 24KB + +**"Transaction reverted"** +- Check constructor arguments +- Ensure sufficient value for payable constructors + +**"Invalid bytecode"** +- Verify artifact path is correct +- Re-run `forge build` diff --git a/base/references/testing.md b/base/references/testing.md new file mode 100644 index 00000000..069f7a42 --- /dev/null +++ b/base/references/testing.md @@ -0,0 +1,317 @@ +# Testing Contracts + +Test smart contracts using Foundry's forge. + +## Basic Test Structure + +```solidity +// test/Counter.t.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function test_SetNumber() public { + counter.setNumber(42); + assertEq(counter.number(), 42); + } +} +``` + +## Run Tests + +```bash +# Run all tests +forge test + +# With verbosity (show logs) +forge test -vvv + +# With maximum verbosity (show all traces) +forge test -vvvvv + +# Run specific test +forge test --match-test test_Increment + +# Run tests in specific file +forge test --match-path test/Counter.t.sol + +# Run with gas report +forge test --gas-report +``` + +## Assertions + +```solidity +// Equality +assertEq(a, b); +assertEq(a, b, "custom error message"); + +// Inequality +assertNotEq(a, b); + +// Greater/less than +assertGt(a, b); // a > b +assertGe(a, b); // a >= b +assertLt(a, b); // a < b +assertLe(a, b); // a <= b + +// Boolean +assertTrue(condition); +assertFalse(condition); +``` + +## Testing Reverts + +```solidity +function test_DecrementReverts() public { + // Expect specific revert message + vm.expectRevert("Cannot decrement below zero"); + counter.decrement(); +} + +function test_CustomErrorReverts() public { + // Expect custom error + vm.expectRevert(abi.encodeWithSelector(MyError.selector, arg1, arg2)); + myContract.doSomething(); +} + +function test_AnyRevert() public { + // Expect any revert + vm.expectRevert(); + myContract.willFail(); +} +``` + +## Fuzz Testing + +Foundry automatically generates random inputs: + +```solidity +function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); +} + +// Bound fuzz input to range +function testFuzz_BoundedInput(uint256 x) public { + x = bound(x, 1, 100); // 1 <= x <= 100 + counter.setNumber(x); + assertGe(counter.number(), 1); + assertLe(counter.number(), 100); +} +``` + +## Cheatcodes (vm) + +### Set msg.sender + +```solidity +function test_OnlyOwner() public { + address owner = address(0x1); + address notOwner = address(0x2); + + // Deploy as owner + vm.prank(owner); + MyContract c = new MyContract(); + + // Call as not owner - should fail + vm.prank(notOwner); + vm.expectRevert("Not owner"); + c.ownerOnly(); + + // Call as owner - should succeed + vm.prank(owner); + c.ownerOnly(); +} +``` + +### Persistent sender + +```solidity +function test_MultipleCalls() public { + vm.startPrank(address(0x1)); + contract.call1(); + contract.call2(); + contract.call3(); + vm.stopPrank(); +} +``` + +### Set block.timestamp + +```solidity +function test_TimeLock() public { + // Warp to specific timestamp + vm.warp(1000); + assertEq(block.timestamp, 1000); + + // Skip forward + skip(100); + assertEq(block.timestamp, 1100); +} +``` + +### Set block.number + +```solidity +function test_BlockNumber() public { + vm.roll(100); + assertEq(block.number, 100); +} +``` + +### Deal ETH + +```solidity +function test_WithETH() public { + address user = address(0x1); + + // Give user 10 ETH + vm.deal(user, 10 ether); + assertEq(user.balance, 10 ether); +} +``` + +### Mock contract calls + +```solidity +function test_MockCall() public { + address target = address(0x1); + + // Mock return value + vm.mockCall( + target, + abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)), + abi.encode(1000) + ); + + uint256 balance = IERC20(target).balanceOf(address(this)); + assertEq(balance, 1000); +} +``` + +## Testing Events + +```solidity +function test_EmitsEvent() public { + // Expect event with specific parameters + vm.expectEmit(true, true, false, true); + emit NumberChanged(42); + + counter.setNumber(42); +} +``` + +## Test Escrow Contract + +```solidity +// test/SimpleEscrow.t.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {SimpleEscrow} from "../src/SimpleEscrow.sol"; + +contract SimpleEscrowTest is Test { + SimpleEscrow public escrow; + address public payer = address(0x1); + address public payee = address(0x2); + + function setUp() public { + vm.deal(payer, 10 ether); + + vm.prank(payer); + escrow = new SimpleEscrow{value: 1 ether}(payee); + } + + function test_Deposit() public view { + assertEq(escrow.payer(), payer); + assertEq(escrow.payee(), payee); + assertEq(escrow.amount(), 1 ether); + assertEq(address(escrow).balance, 1 ether); + } + + function test_Release() public { + uint256 payeeBefore = payee.balance; + + vm.prank(payer); + escrow.release(); + + assertEq(payee.balance, payeeBefore + 1 ether); + assertTrue(escrow.released()); + } + + function test_OnlyPayerCanRelease() public { + vm.prank(payee); + vm.expectRevert("Only payer can release"); + escrow.release(); + } + + function test_Refund() public { + uint256 payerBefore = payer.balance; + + vm.prank(payee); + escrow.refund(); + + assertEq(payer.balance, payerBefore + 1 ether); + assertTrue(escrow.released()); + } + + function test_CannotDoubleRelease() public { + vm.prank(payer); + escrow.release(); + + vm.prank(payer); + vm.expectRevert("Already released"); + escrow.release(); + } +} +``` + +## Gas Reports + +```bash +forge test --gas-report +``` + +Output shows gas usage per function. + +## Coverage + +```bash +forge coverage +``` + +## Debugging + +```solidity +import {console} from "forge-std/Test.sol"; + +function test_Debug() public { + console.log("Value:", counter.number()); + console.log("Address:", address(this)); + console.logBytes(data); +} +``` + +Run with `-vvv` to see console output. + +## Resources + +- [Foundry Book - Testing](https://book.getfoundry.sh/forge/tests) +- [Forge Cheatcodes](https://book.getfoundry.sh/cheatcodes/) diff --git a/base/references/verification.md b/base/references/verification.md new file mode 100644 index 00000000..902fe9da --- /dev/null +++ b/base/references/verification.md @@ -0,0 +1,195 @@ +# Contract Verification + +Verify contracts on Basescan after deployment. + +## Why Verify? + +- Source code is publicly visible +- Users can read and trust the contract +- Enables Basescan's "Read/Write Contract" UI +- Required for most serious projects + +## Get Basescan API Key + +1. Go to [Basescan](https://basescan.org/) (or [sepolia.basescan.org](https://sepolia.basescan.org/) for testnet) +2. Create an account +3. Go to API Keys +4. Generate a new API key + +Add to `.env`: +```bash +BASESCAN_API_KEY=your_api_key +``` + +## Verify with Forge + +### Basic Verification + +```bash +forge verify-contract \ + --chain-id 84532 \ + --watch \ + --etherscan-api-key $BASESCAN_API_KEY \ + \ + src/Counter.sol:Counter +``` + +### With Constructor Arguments + +```bash +forge verify-contract \ + --chain-id 84532 \ + --watch \ + --etherscan-api-key $BASESCAN_API_KEY \ + --constructor-args $(cast abi-encode "constructor(string,string,uint256)" "My Token" "MTK" 1000000) \ + \ + src/MyToken.sol:MyToken +``` + +### With Optimizer Settings + +```bash +forge verify-contract \ + --chain-id 84532 \ + --num-of-optimizations 200 \ + --watch \ + --etherscan-api-key $BASESCAN_API_KEY \ + \ + src/Counter.sol:Counter +``` + +## Chain IDs + +| Network | Chain ID | +|---------|----------| +| Base Mainnet | 8453 | +| Base Sepolia | 84532 | + +## Configure in foundry.toml + +```toml +[etherscan] +base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" } +base = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api" } +``` + +Then verify with: +```bash +forge verify-contract \ + --chain base-sepolia \ + --watch \ + \ + src/Counter.sol:Counter +``` + +## Deploy and Verify in One Command + +```bash +forge script script/Deploy.s.sol \ + --rpc-url $BASE_SEPOLIA_RPC \ + --broadcast \ + --verify \ + --etherscan-api-key $BASESCAN_API_KEY \ + -vvvv +``` + +## Verify Existing Contract + +If you deployed earlier and need to verify: + +```bash +# For Sepolia +forge verify-contract \ + --chain-id 84532 \ + --etherscan-api-key $BASESCAN_API_KEY \ + 0xYourContractAddress \ + src/YourContract.sol:YourContract + +# For Mainnet +forge verify-contract \ + --chain-id 8453 \ + --etherscan-api-key $BASESCAN_API_KEY \ + 0xYourContractAddress \ + src/YourContract.sol:YourContract +``` + +## Check Verification Status + +```bash +forge verify-check \ + --chain-id 84532 \ + +``` + +## Common Issues + +**"Unable to verify"** +- Ensure compiler version matches +- Check optimizer settings match deployment +- Verify constructor arguments are correct + +**"Contract source code already verified"** +- Contract was already verified (this is fine) + +**"Invalid API key"** +- Check BASESCAN_API_KEY is set correctly +- Ensure you're using the right key for the network (mainnet vs testnet share keys) + +**"Compiler version mismatch"** +- Check solc version in foundry.toml +- Use `--compiler-version` flag if needed + +## Verification Script + +```typescript +// scripts/verify.ts +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); + +interface VerifyConfig { + address: string; + contract: string; + constructorArgs?: string; + network: "base-sepolia" | "base"; +} + +async function verify(config: VerifyConfig) { + const chainId = config.network === "base-sepolia" ? 84532 : 8453; + + let cmd = `forge verify-contract \ + --chain-id ${chainId} \ + --watch \ + --etherscan-api-key ${process.env.BASESCAN_API_KEY}`; + + if (config.constructorArgs) { + cmd += ` --constructor-args ${config.constructorArgs}`; + } + + cmd += ` ${config.address} ${config.contract}`; + + console.log("Running:", cmd); + + try { + const { stdout, stderr } = await execAsync(cmd); + console.log(stdout); + if (stderr) console.error(stderr); + } catch (error) { + console.error("Verification failed:", error); + } +} + +// Verify Counter +verify({ + address: "0x...", + contract: "src/Counter.sol:Counter", + network: "base-sepolia", +}).catch(console.error); +``` + +## Resources + +- [Basescan](https://basescan.org/) +- [Basescan Sepolia](https://sepolia.basescan.org/) +- [Forge Verify Docs](https://book.getfoundry.sh/reference/forge/forge-verify-contract)