defi-protocol-templates について
defi-protocol-templatesは、ステーキング、AMM、ガバナンス、レンディング、フラッシュローンなどのDeFiコントラクトパターンを学ぶ開発者向けスキルです。インライン例は不完全な教材で、参照資料も同梱されていません。デプロイ前のテストと独立したレビューを重視します。
主な機能
- 01ステーキングとAMMのパターン例
- 02ガバナンストークンとフラッシュローンの例
- 03テストとセキュリティレビューの指針
- 04不足している資料とコントラクトの制約の説明
活用シーン
- 01一般的なDeFiコントラクト設計の学習
- 02ステーキングや流動性プール案の試作
- 03実装前のセキュリティ上の注意点の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill defi-protocol-templatesインストール後、Claude Code で/defi-protocol-templatesを入力して読み込まれたか確認します。
詳細
補足資料の利用可否
このカタログのコピーには以下のインラインガイダンスが含まれていますが、次の補足パスは含まれていません。これらは外部の前提条件であり、同梱ファイルではありません。
<support-root>/references/staking.md<support-root>/references/liquidity-pools.md<support-root>/references/governance-tokens.md<support-root>/references/lending-protocols.md<support-root>/references/flash-loans.md<support-root>/assets/staking-contract.sol<support-root>/assets/amm-contract.sol<support-root>/assets/governance-token.sol<support-root>/assets/lending-protocol.sol
これらのパスに依存する手順を実行する前に、元の補足資料または既存のインストール環境を確認してください。このカタログには、検証済みのダウンロード元が記載されていません。各パスをそのインストール環境を基準に解決し、内容を確認して、適用されるライセンスと実行上の前提条件を検証してください。必要なファイルが存在することを確認するまでは、コマンドを実行したり、参照先の手順に従ったと主張したりしないでください。不足しているスクリプトを捏造したり、同じ名前の無関係なファイルを同等のものとして扱ったりしないでください。ここで完全に説明されている独立した手順は、ユーザーが許可した範囲内で進められます。依存する手順は利用不可として報告してください。
DeFi プロトコルのテンプレート
ステーキング、AMM、ガバナンス、レンディング、フラッシュローンなど、一般的な DeFi プロトコルの教育用サンプルです。
このスキルを使用しない場合
- タスクが DeFi プロトコルのテンプレートと無関係な場合
- この範囲外の別のドメインやツールが必要な場合
手順
- 目標、制約、必要な入力を明確にします。
- 関連するベストプラクティスを適用し、結果を検証します。
- 実行可能な手順と検証方法を提示します。
このスキルを使用する場合
- 報酬分配機能を備えたステーキングプラットフォームを構築する場合
- AMM(自動マーケットメーカー)プロトコルを実装する場合
- ガバナンストークンシステムを作成する場合
- レンディング/借り入れプロトコルを開発する場合
- フラッシュローン機能を統合する場合
- イールドファーミングプラットフォームを立ち上げる場合
これらのインラインコントラクトは不完全な教育用サンプルであり、監査済みでも資金が投入されたプロトコルでもありません。OpenZeppelin4.x のインポート/継承 API を使用しているため、確認済みの互換バージョンを固定し、5.x を無条件にインストールしないでください。ユーザー資金を使ってデプロイしないでください。ステーキングには、資金や期間を管理する仕組みのない、固定で上限のない報酬率が設定されています。AMM には、本番環境に必要な準備金、オラクル、LP の安全策がありません。ガバナーには、定足数、タイムロック、アクションのペイロードがありません。フラッシュローンには、レシーバー固有の信頼されたコールバックが必要です。デプロイを設計する前にコンパイルし、アカウンティング、リエントランシー、丸め誤差を対象としたプロパティテスト/ファズテストを行い、独立したレビューを受けてください。トークンの単位/小数桁数は異なり、送金手数料付きトークンとリベーストークンはサポートされていません。
ステーキングコントラクト
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract StakingRewards is ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
IERC20 public stakingToken;
IERC20 public rewardsToken;
uint256 public rewardRate = 100; // Rewards per second
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
mapping(address => uint256) public balances;
uint256 private _totalSupply;
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 reward);
constructor(address _stakingToken, address _rewardsToken) {
stakingToken = IERC20(_stakingToken);
rewardsToken = IERC20(_rewardsToken);
}
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function rewardPerToken() public view returns (uint256) {
if (_totalSupply == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored +
((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / _totalSupply;
}
function earned(address account) public view returns (uint256) {
return (balances[account] *
(rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18 +
rewards[account];
}
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Cannot stake 0");
_totalSupply += amount;
balances[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
require(amount > 0, "Cannot withdraw 0");
_totalSupply -= amount;
balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
function getReward() public nonReentrant updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardsToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
function exit() external {
withdraw(balances[msg.sender]);
getReward();
}
}
AMM(自動マーケットメーカー)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SimpleAMM is ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public token0;
IERC20 public token1;
uint256 public reserve0;
uint256 public reserve1;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Mint(address indexed to, uint256 amount);
event Burn(address indexed from, uint256 amount);
event Swap(address indexed trader, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out);
constructor(address _token0, address _token1) {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}
function addLiquidity(uint256 amount0, uint256 amount1) external nonReentrant returns (uint256 shares) {
token0.safeTransferFrom(msg.sender, address(this), amount0);
token1.safeTransferFrom(msg.sender, address(this), amount1);
if (totalSupply == 0) {
shares = sqrt(amount0 * amount1);
} else {
shares = min(
(amount0 * totalSupply) / reserve0,
(amount1 * totalSupply) / reserve1
);
}
require(shares > 0, "Shares = 0");
_mint(msg.sender, shares);
_update(
token0.balanceOf(address(this)),
token1.balanceOf(address(this))
);
emit Mint(msg.sender, shares);
}
function removeLiquidity(uint256 shares) external nonReentrant returns (uint256 amount0, uint256 amount1) {
uint256 bal0 = token0.balanceOf(address(this));
uint256 bal1 = token1.balanceOf(address(this));
amount0 = (shares * bal0) / totalSupply;
amount1 = (shares * bal1) / totalSupply;
require(amount0 > 0 && amount1 > 0, "Amount0 or amount1 = 0");
_burn(msg.sender, shares);
_update(bal0 - amount0, bal1 - amount1);
token0.safeTransfer(msg.sender, amount0);
token1.safeTransfer(msg.sender, amount1);
emit Burn(msg.sender, shares);
}
function swap(address tokenIn, uint256 amountIn, uint256 minAmountOut, uint256 deadline) external nonReentrant returns (uint256 amountOut) {
require(block.timestamp <= deadline && amountIn > 0, "Expired or zero input");
require(tokenIn == address(token0) || tokenIn == address(token1), "Invalid token");
bool isToken0 = tokenIn == address(token0);
(IERC20 tokenIn_, IERC20 tokenOut, uint256 resIn, uint256 resOut) = isToken0
? (token0, token1, reserve0, reserve1)
: (token1, token0, reserve1, reserve0);
tokenIn_.safeTransferFrom(msg.sender, address(this), amountIn);
// 0.3% fee
uint256 amountInWithFee = (amountIn * 997) / 1000;
amountOut = (resOut * amountInWithFee) / (resIn + amountInWithFee);
require(amountOut >= minAmountOut && amountOut > 0, "Slippage or zero output");
tokenOut.safeTransfer(msg.sender, amountOut);
_update(
token0.balanceOf(address(this)),
token1.balanceOf(address(this))
);
emit Swap(msg.sender, isToken0 ? amountIn : 0, isToken0 ? 0 : amountIn, isToken0 ? 0 : amountOut, isToken0 ? amountOut : 0);
}
function _mint(address to, uint256 amount) private {
balanceOf[to] += amount;
totalSupply += amount;
}
function _burn(address from, uint256 amount) private {
balanceOf[from] -= amount;
totalSupply -= amount;
}
function _update(uint256 res0, uint256 res1) private {
reserve0 = res0;
reserve1 = res1;
}
function sqrt(uint256 y) private pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
function min(uint256 x, uint256 y) private pure returns (uint256) {
return x <= y ? x : y;
}
}
ガバナンストークン
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract GovernanceToken is ERC20Votes, Ownable {
constructor() ERC20("Governance Token", "GOV") ERC20Permit("Governance Token") {
_mint(msg.sender, 1000000 * 10**decimals());
}
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount) internal override(ERC20Votes) {
super._mint(to, amount);
}
function _burn(address account, uint256 amount) internal override(ERC20Votes) {
super._burn(account, amount);
}
}
contract Governor is Ownable {
GovernanceToken public governanceToken;
struct Proposal {
uint256 id;
address proposer;
string description;
uint256 forVotes;
uint256 againstVotes;
uint256 startBlock;
uint256 endBlock;
bool executed;
mapping(address => bool) hasVoted;
}
uint256 public proposalCount;
mapping(uint256 => Proposal) public proposals;
uint256 public votingPeriod = 17280; // ~3 days in blocks
uint256 public proposalThreshold = 100000 * 10**18;
event ProposalCreated(uint256 indexed proposalId, address proposer, string description);
event VoteCast(address indexed voter, uint256 indexed proposalId, bool support, uint256 weight);
event ProposalExecuted(uint256 indexed proposalId);
constructor(address _governanceToken) {
governanceToken = GovernanceToken(_governanceToken);
}
function propose(string memory description) external returns (uint256) {
require(
governanceToken.getPastVotes(msg.sender, block.number - 1) >= proposalThreshold,
"Proposer votes below threshold"
);
proposalCount++;
Proposal storage newProposal = proposals[proposalCount];
newProposal.id = proposalCount;
newProposal.proposer = msg.sender;
newProposal.description = description;
newProposal.startBlock = block.number;
newProposal.endBlock = block.number + votingPeriod;
emit ProposalCreated(proposalCount, msg.sender, description);
return proposalCount;
}
function vote(uint256 proposalId, bool support) external {
Proposal storage proposal = proposals[proposalId];
require(proposalId > 0 && proposalId <= proposalCount, "Unknown proposal");
require(block.number > proposal.startBlock, "Snapshot not yet historical");
require(block.number <= proposal.endBlock, "Voting ended");
require(!proposal.hasVoted[msg.sender], "Already voted");
uint256 weight = governanceToken.getPastVotes(msg.sender, proposal.startBlock);
require(weight > 0, "No voting power");
proposal.hasVoted[msg.sender] = true;
if (support) {
proposal.forVotes += weight;
} else {
proposal.againstVotes += weight;
}
emit VoteCast(msg.sender, proposalId, support, weight);
}
function execute(uint256 proposalId) external {
Proposal storage proposal = proposals[proposalId];
require(block.number > proposal.endBlock, "Voting not ended");
require(!proposal.executed, "Already executed");
require(proposal.forVotes > proposal.againstVotes, "Proposal failed");
revert("Teaching stub: no action execution/quorum/timelock implemented");
}
}
フラッシュローン
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IFlashLoanReceiver {
function executeOperation(
address asset,
uint256 amount,
uint256 fee,
bytes calldata params
) external returns (bool);
}
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract FlashLoanProvider is ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public token;
uint256 public feePercentage = 9; // 0.09% fee
event FlashLoan(address indexed borrower, uint256 amount, uint256 fee);
constructor(address _token) {
token = IERC20(_token);
}
function flashLoan(
address receiver,
uint256 amount,
bytes calldata params
) external nonReentrant {
uint256 balanceBefore = token.balanceOf(address(this));
require(balanceBefore >= amount, "Insufficient liquidity");
uint256 fee = (amount * feePercentage) / 10000;
// Send tokens to receiver
token.safeTransfer(receiver, amount);
// Execute callback
require(
IFlashLoanReceiver(receiver).executeOperation(
address(token),
amount,
fee,
params
),
"Flash loan failed"
);
// Verify repayment
uint256 balanceAfter = token.balanceOf(address(this));
require(balanceAfter >= balanceBefore + fee, "Flash loan not repaid");
emit FlashLoan(receiver, amount, fee);
}
}
// Example flash loan receiver
contract FlashLoanReceiver is IFlashLoanReceiver {
using SafeERC20 for IERC20;
address public immutable provider;
constructor(address trustedProvider) { provider = trustedProvider; }
function executeOperation(
address asset,
uint256 amount,
uint256 fee,
bytes calldata params
) external override returns (bool) {
require(msg.sender == provider, "Untrusted lender");
// Decode params and execute arbitrage, liquidation, etc.
// ...
// This lender checks its final balance; approval alone does not repay.
IERC20(asset).safeTransfer(provider, amount + fee);
return true;
}
}
リソース
- references/staking.md:ステーキングの仕組みと報酬分配
- references/liquidity-pools.md:AMM の数学的基礎と価格設定
- references/governance-tokens.md:ガバナンスと投票システム
- references/lending-protocols.md:レンディング/借り入れの実装
- references/flash-loans.md:フラッシュローンのセキュリティとユースケース
- assets/staking-contract.sol:本番用ステーキングテンプレート
- assets/amm-contract.sol:完全な AMM 実装
- assets/governance-token.sol:ガバナンスシステム
- assets/lending-protocol.sol:レンディングプラットフォームのテンプレート
ベストプラクティス
- 実績のあるライブラリを使用する:OpenZeppelin、Solmate
- 徹底的にテストする:単体テスト、統合テスト、ファズテスト
- ローンチ前に監査を受ける:専門家によるセキュリティ監査
- シンプルに始める:まず MVP を作成し、機能を段階的に追加する
- 監視する:コントラクトの健全性とユーザーのアクティビティを追跡する
- アップグレード可能性:アップグレードにプロキシパターンを検討する
- 緊急時の制御:重大な問題に備えて一時停止の仕組みを設ける
一般的な DeFi パターン
- 時間加重平均価格(TWAP):価格オラクルへの耐性
- 流動性マイニング:流動性の提供を促進します
- ベスティング:トークンをロックし、段階的にリリースします
- マルチシグ:重要な操作に複数の署名を必須とします
- タイムロック:ガバナンスの決定の実行を遅らせます
制限事項
- 上記の説明にある範囲にタスクが明確に該当する場合に限り、このスキルを使用してください。
- 出力を、環境固有の検証、テスト、または専門家によるレビューの代わりとして扱わないでください。
- 必要な入力、権限、安全上の境界、または成功基準が不足している場合は、作業を中断して確認を求めてください。