이 레벨에서는 CryptoVault라는 특별한 기능을 가진 컨트랙트가 등장합니다.
그 중 핵심 기능은 sweepToken 함수인데, 이 함수는 일반적으로 컨트랙트에 “갇혀 있는” 토큰을 회수할 때 사용됩니다.
CryptoVault에는 기본(underlying) 토큰이라는 것이 있는데, 이 토큰은 CryptoVault의 핵심 로직에서 사용되므로 회수할 수 없게 설계되어 있습니다.
하지만, 이 기본 토큰이 아닌 다른 모든 토큰은 회수가 가능합니다.
현재 상황:
• 기본 토큰은 DoubleEntryPoint 컨트랙트에서 구현된 DET 토큰이며, CryptoVault는 DET 토큰 100개를 보유하고 있습니다.
• CryptoVault는 LegacyToken (LGT) 100개도 보유하고 있습니다.
이번 레벨에서의 목표:
1. CryptoVault의 버그를 찾아내고,
2. 그 버그를 악용해 토큰이 빠져나가지 않도록 보호해야 합니다.
⸻
CryptoVault에는 또 다른 중요한 요소로 Forta라는 컨트랙트가 있습니다.
• Forta는 사용자가 자신의 탐지 봇(detection bot) 컨트랙트를 등록할 수 있는 구조입니다.
• Forta는 탈중앙화된 커뮤니티 기반 모니터링 네트워크로, DeFi·NFT·거버넌스·브리지 등 다양한 Web3 시스템에서 발생할 수 있는 위협이나 이상 징후를 최대한 빨리 탐지하는 역할을 합니다.
당신의 역할:
• 탐지 봇을 구현하고, Forta 컨트랙트에 등록하세요.
• 봇이 올바르게 경고를 발생시켜야만 잠재적 공격이나 버그 악용을 방지할 수 있습니다.
힌트:
• Double Entry Point(이중 진입점)이 토큰 컨트랙트에서 어떻게 동작하는지 이해하는 것이 도움이 됩니다.
요약: IDetectionBot을 구현한 뒤 Forta 컨트랙트에 등록하여 자금이 유출되는 것을 막아라
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/access/Ownable.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
interface DelegateERC20 {
function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
}
interface IDetectionBot {
function handleTransaction(address user, bytes calldata msgData) external;
}
interface IForta {
function setDetectionBot(address detectionBotAddress) external;
function notify(address user, bytes calldata msgData) external;
function raiseAlert(address user) external;
}
contract Forta is IForta {
mapping(address => IDetectionBot) public usersDetectionBots;
mapping(address => uint256) public botRaisedAlerts;
function setDetectionBot(address detectionBotAddress) external override {
usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
}
function notify(address user, bytes calldata msgData) external override {
if (address(usersDetectionBots[user]) == address(0)) return;
try usersDetectionBots[user].handleTransaction(user, msgData) {
return;
} catch {}
}
function raiseAlert(address user) external override {
if (address(usersDetectionBots[user]) != msg.sender) return;
botRaisedAlerts[msg.sender] += 1;
}
}
contract CryptoVault {
address public sweptTokensRecipient;
IERC20 public underlying;
constructor(address recipient) {
sweptTokensRecipient = recipient;
}
function setUnderlying(address latestToken) public {
require(address(underlying) == address(0), "Already set");
underlying = IERC20(latestToken);
}
/*
...
*/
function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
}
contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
DelegateERC20 public delegate;
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
delegate = newContract;
}
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);
}
}
}
contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
address public cryptoVault;
address public player;
address public delegatedFrom;
Forta public forta;
constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) {
delegatedFrom = legacyToken;
forta = Forta(fortaAddress);
player = playerAddress;
cryptoVault = vaultAddress;
_mint(cryptoVault, 100 ether);
}
modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}
function delegateTransfer(address to, uint256 value, address origSender)
public
override
onlyDelegateFrom
fortaNotify
returns (bool)
{
_transfer(origSender, to, value);
return true;
}
}
우리는 봇을 만들어 DET 자금이 유출되는 것을 막아야한다. 따라서 DET 를 인출할 수 있는 코드를 살펴보겠다.
contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
DelegateERC20 public delegate;
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
delegate = newContract;
}
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);
}
}
}
LegacyToken contract는 transfer() 함수를 이용하여 delegate.delegateTransfer() 함수를 호출하여 transfer 작업을 진행하고 있다. 이떄 delegate는 DoubleEntryPoint 를 의미한다.
즉, DoubleEntryPoint contract 내부의 delegateTransfer() 함수를 호출한다.
function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}
delegateTransfer() 함수를 살펴보면 _transfer 작업을 진행하고 있는데 이 함수에 접근하려면 onlyDelegateFrom, fortaNotify 두 가지 modifier를 통과해야 한다.
modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}
onlyDelegateFrom() 는 legacy contract에서 호출하면 문제없이 호출될 것이다.
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}