-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
contractSmart contract developmentSmart contract development
Description
一行説明
EIP-4337スマートアカウント(AA)ユーザー向けに、事前承認を利用した分配送金機能を実装する
詳細
背景
- issue #15で実装する
transferWithPermitはEOAユーザーにとって便利(オフチェーン署名でガスレス承認) - しかし、EIP-4337のスマートアカウントユーザーはpermit署名ができないため、通常のapproveが必要
- AAユーザーがapproveとRouterの実行を1つのUserOperationでバッチ実行できるよう、permit不要の関数が必要
実装内容
事前にFORTokenへapproveされていることを前提として、トークンの分配送金を実行する関数を追加。
関数シグネチャ:
function transferWithDistribution(
address from,
address recipient,
uint256 amount
) external whenNotPaused nonReentrant;処理フロー:
- 入力値の検証(amount、recipient)
- 分配額の計算(fundRatio、burnRatio、recipientAmount)
- 3つの宛先への
transferFrom実行(fund、burn、recipient) TransferWithDistributionイベントの発行
AAユーザーのユースケース
Pimlico等のバンドラーで、approve + transferWithDistributionを1つのUserOperationとしてバッチ実行:
await smartAccount.executeBatch([
// 1. FORTokenへの承認
{ to: forToken, data: approve(router, amount) },
// 2. 分配送金の実行
{ to: router, data: transferWithDistribution(from, recipient, amount) }
]);EOAユーザーのユースケース
事前にapproveしておけば、EOAユーザーも使用可能(ただしpermit版の方が便利):
// 事前承認(1回のみ、初回または追加承認時)
await forToken.approve(router, maxAmount);
// 分配送金実行
await router.transferWithDistribution(account, recipient, amount);要件
-
transferWithDistribution(address from, address recipient, uint256 amount)関数の実装 - 入力値検証(amount != 0、recipient != address(0))
- 分配計算ロジックの実装(issue #15と共通化)
- 3つの宛先への
transferFrom実行 -
TransferWithDistributionイベントの発行(issue #15と共通イベント) -
whenNotPausedおよびnonReentrantモディファイアの適用 - Hardhatでコンパイル成功
- 包括的なテストの作成(正常系、異常系、エッジケース)
補足
Issue #15との共通化ポイント
以下の要素はtransferWithPermit(issue #15)と共通化できます:
-
分配計算ロジック
uint256 fundAmount = (amount * fundRatio) / 10000; uint256 burnAmount = (amount * burnRatio) / 10000; uint256 recipientAmount = amount - fundAmount - burnAmount;
→ 内部関数
_calculateDistribution(uint256 amount)として共通化を検討 -
トークン送金処理
if (fundAmount > 0) token.transferFrom(from, fundWallet, fundAmount); if (burnAmount > 0) token.transferFrom(from, BURN_ADDRESS, burnAmount); if (recipientAmount > 0) token.transferFrom(from, recipient, recipientAmount);
→ 内部関数
_executeDistribution(address from, address recipient, uint256 amount)として共通化を検討 -
TransferWithDistributionイベント
- 両関数で同じイベント定義を使用
-
入力値検証
- amount != 0、recipient != address(0)のチェックは共通
-
カスタムエラー
InvalidAmount()、InvalidRecipient()は共通
リファクタリング案
// 内部関数(共通化)
function _calculateDistribution(uint256 amount)
internal
view
returns (uint256 fundAmount, uint256 burnAmount, uint256 recipientAmount)
{
fundAmount = (amount * fundRatio) / 10000;
burnAmount = (amount * burnRatio) / 10000;
recipientAmount = amount - fundAmount - burnAmount;
}
function _executeDistribution(
address from,
address recipient,
uint256 amount,
uint256 fundAmount,
uint256 burnAmount,
uint256 recipientAmount
) internal {
IERC20 token = IERC20(forToken);
if (fundAmount > 0) {
require(token.transferFrom(from, fundWallet, fundAmount), "Fund transfer failed");
}
if (burnAmount > 0) {
require(token.transferFrom(from, BURN_ADDRESS, burnAmount), "Burn transfer failed");
}
if (recipientAmount > 0) {
require(token.transferFrom(from, recipient, recipientAmount), "Recipient transfer failed");
}
}
// 公開関数(permit版)
function transferWithPermit(...) external whenNotPaused nonReentrant {
// 入力値検証
if (amount == 0) revert InvalidAmount();
if (recipient == address(0)) revert InvalidRecipient();
// Permit実行
IERC20Permit(forToken).permit(from, address(this), amount, deadline, v, r, s);
// 分配計算
(uint256 fundAmount, uint256 burnAmount, uint256 recipientAmount) = _calculateDistribution(amount);
// 分配実行
_executeDistribution(from, recipient, amount, fundAmount, burnAmount, recipientAmount);
// イベント発行
emit TransferWithDistribution(msg.sender, from, recipient, amount, fundAmount, burnAmount, recipientAmount);
}
// 公開関数(事前承認版)
function transferWithDistribution(...) external whenNotPaused nonReentrant {
// 入力値検証
if (amount == 0) revert InvalidAmount();
if (recipient == address(0)) revert InvalidRecipient();
// 分配計算
(uint256 fundAmount, uint256 burnAmount, uint256 recipientAmount) = _calculateDistribution(amount);
// 分配実行
_executeDistribution(from, recipient, amount, fundAmount, burnAmount, recipientAmount);
// イベント発行
emit TransferWithDistribution(msg.sender, from, recipient, amount, fundAmount, burnAmount, recipientAmount);
}実装順序の提案
- Issue #15を実装(
transferWithPermit) - このissueを実装する際に、共通ロジックを内部関数として抽出
- 両方の関数が同じ内部関数を使用するようリファクタリング
技術スタック
- Solidity ^0.8.28
- OpenZeppelin Contracts(IERC20、ReentrancyGuard)
- EIP-4337対応
依存関係
- FORTokenのERC20実装完了後
- Routerの基本構造、分配比率管理実装完了後
- Issue Routerコントラクト: permit経由の分配送金実装 #15(transferWithPermit)の実装と並行または後で実装可能
関連Issue
- Routerコントラクト: permit経由の分配送金実装 #15 - Routerコントラクト: permit経由の分配送金実装(EOA向け)
フロントエンド実装時の考慮事項
// ウォレットタイプに応じた実装切り替え
if (isEOAWallet(account)) {
// EOAユーザー: permit版を優先(ガスレス承認)
const { v, r, s } = await signPermit(...);
await router.transferWithPermit(from, recipient, amount, deadline, v, r, s);
} else if (isSmartAccount(account)) {
// AAユーザー: 事前承認版をバッチ実行
await smartAccount.executeBatch([
{ to: forToken, data: approveCall },
{ to: router, data: transferWithDistributionCall }
]);
}テストケース
Issue #15と同様の包括的なテストが必要:
- 正常系: 標準的な比率での送金成功
- 分配検証: 各宛先への正確な分配額
- エラーハンドリング: ゼロ額、ゼロアドレス受取人、残高不足、承認不足
- Pausable: 一時停止中のコントラクトでrevert
- イベント発行: TransferWithDistributionイベントの検証
- エッジケース: from == recipient、大きな額、比率境界値
推定工数
1-2日(issue #15の実装パターンを流用できるため)
Metadata
Metadata
Assignees
Labels
contractSmart contract developmentSmart contract development