Appearance
CTF問題解説:DoubleEntryPoint - 二重エントリーポイント脆弱性と検知ボット防御
背景紹介
DoubleEntryPoint は、OpenZeppelin と Forta によって設計されたイーサリアムの CTF チャレンジであり、DeFi システムでよく見られる「二重エントリーポイント(Double Entry Point)」の脆弱性パターンを示しています。この課題では、ERC20 トークンのプロキシ構造、コントラクト間の安全な相互作用、そして監視ネットワークを用いた攻撃防御の仕組みを理解する必要があります。
このチャレンジでは、CryptoVault コントラクトが登場し、「詰まった」トークンを引き出すための sweepToken 機能を持っています。しかし、このコントラクトには微妙なセキュリティ上の欠陥があり、攻撃者に悪用される可能性があります。プレイヤーの目的は、検知ボット(Detection Bot)を実装し、それを Forta 監視ネットワークに登録することで、この攻撃を防ぐことです。
技術アーキテクチャ分析
コントラクト構成
システムは以下の主要コンポーネントで構成されています:
- DoubleEntryPoint (DET) - メイントークン(DelegateERC20 を実装)
- LegacyToken (LGT) - 旧トークン(新コントラクトへ委譲可能)
- CryptoVault - トークン保管用の金庫コントラクト
- Forta - 監視ネットワーク
- Hack - プレイヤーが実装する検知ボット
CryptoVault の脆弱性
solidity
function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
一見すると、underlying(DET)の転送は禁止されていますが、このチェックは不完全です。
LegacyToken の委譲ロジック
solidity
function transfer(address to, uint256 value) public override returns (bool) {
if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
}
委譲先が設定されている場合、transfer は直接実行されず、delegateTransfer が呼ばれます。
DoubleEntryPoint の delegateTransfer
solidity
function delegateTransfer(address to, uint256 value, address origSender)
public
override
onlyDelegateFrom
fortaNotify
returns (bool)
{
_transfer(origSender, to, value);
return true;
}
重要ポイント:
origSenderからトークンを移転する- Forta による監視が組み込まれている
脆弱性と攻撃フロー
脆弱性の本質
CryptoVault は「DET を直接転送する」ことだけを防いでいますが、「間接的な転送」は防いでいません。
攻撃手順
攻撃者 → sweepToken(LGT) を呼び出す
↓
CryptoVault → LGT.transfer() を実行
↓
LegacyToken → delegateTransfer を呼び出す
↓
DoubleEntryPoint.delegateTransfer(...)
↓
DET が CryptoVault から攻撃者へ転送される
なぜ成立するのか?
token != underlyingは LGT に対しては通過- しかし LGT は DET に委譲している
- 結果として DET が動く
👉 これが「二重エントリーポイント」脆弱性
Forta 防御メカニズム
Forta の仕組み
solidity
function notify(address user, bytes calldata msgData) external override {
if (address(usersDetectionBots[user]) == address(0)) return;
try usersDetectionBots[user].handleTransaction(user, msgData) {
return;
} catch {}
}
検知ボットにトランザクションを通知します。
fortaNotify 修飾子
solidity
modifier fortaNotify() {
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
forta.notify(player, msg.data);
_;
if (forta.botRaisedAlerts(detectionBot) > previousValue)
revert("Alert has been triggered, reverting");
}
👉 アラートが発生するとトランザクションをロールバック
検知ボットの実装
コアアイデア
検知すべき条件:
👉 origSender == CryptoVault
実装コード
solidity
contract Hack is IDetectionBot {
IForta private immutable target;
address private immutable cryptoVault;
constructor(address _target, address _cryptoVault) {
target = IForta(_target);
cryptoVault = _cryptoVault;
}
function handleTransaction(address user, bytes calldata msgData) external {
require(msg.sender == address(target), "Unauthorized");
if (msgData.length >= 68) {
bytes4 selector = bytes4(msgData[:4]);
if (selector == DoubleEntryPoint.delegateTransfer.selector) {
(address to, uint256 value, address origSender) = abi.decode(
msgData[4:],
(address, uint256, address)
);
if (origSender == cryptoVault) {
target.raiseAlert(user);
}
}
}
}
}
テスト検証
攻撃がブロックされるか
typescript
await expect(
cryptoVault.sweepToken(legacyToken.address)
).to.be.revertedWith("Alert has been triggered, reverting");
正常動作は維持されるか
typescript
await legacyToken.transfer(attacker.address, amount);
セキュリティベストプラクティス
1. 間接経路を考慮する
直接のチェックだけでは不十分。委譲やフックも考慮する。
2. モニタリング導入
Forta のようなリアルタイム監視は有効。
3. 安全なデコード
abi.decode を優先する。
4. アクセス制御
呼び出し元を厳密に制限する。
5. テスト強化
攻撃シナリオを必ず含める。
結論
DoubleEntryPoint チャレンジは、「間接的な呼び出し経路」を利用した典型的な DeFi 脆弱性を示しています。
単純なチェックでは不十分であり、 👉 「実際にどのトークンが動くか」まで追跡する必要がある
また、Forta のような監視レイヤーを導入することで:
- コントラクトのロジックを変更せずに防御可能
- 実運用に近いセキュリティモデルを実現
この問題は、スマートコントラクトセキュリティにおいて非常に重要な教訓を与えてくれます。