이번 문제는 Detection Bot의 특정 기능을 구현해 등록하고 Vault에 존재하는 DET Token이 탈취 당하는 것을 막는것이 목표입니다.
아래는 문제 컨트랙트 입니다.
// 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;
}
}
구성 컨트랙트는 아래와 같습니다.
- Forta
- Legacy Token
- Vault
- Double EntryPoint(DET)
간단하게 살펴보면 Forta는 Detection Bot을 등록, 관리 실행하는 컨트랙트입니다. 저희가 Dectection Bot을 구현하면 이 컨트랙트에 등록하면 됩니다.
Legacy Token은 ERC20 표준을 따르고 있는 토큰입니다. 특이한 점이 있다면 delegate를 등록해놓고 만약 등록되어 있다면 Legacy Token이 아닌 delegate Token을 대신 전송하고 있습니다.
Vault 부분은 중간이 생략되어 있긴하지만 구현되어 있는 부분을 살펴보면 특정 토큰은 거래 불가 토큰으로 지정하고 정해진 Recipient에게 Vault에 존재하는 토큰의 수량을 모두 전송하는 기능을 가지고 있습니다.
Double EntryPoint는 ERC20 표준의 DET를 상속받고 있으며 Forta 등록된 Detection Bot을 통해 거래를 확인하고 이상이 있다면 revert 시키고 있습니다. 그리고 등록되어 있는 delegateFrom으로부터 delegateTransfer call이 오면 DET를 전송하고 있습니다.
문제에서 취약점을 찾아보자면 분명 문제에서 DET은 누군가에게 전송이 되면 안된다고 했지만 마지막 DoubleEntryPoint를 보면 delegateTransfer를 통해 누군가에게 전송이 되고 있습니다.
function delegateTransfer(address to, uint256 value, address origSender)
public
override
onlyDelegateFrom
fortaNotify
returns (bool)
{
_transfer(origSender, to, value);
return true;
}
저희가 막아야할 부분이 이 부분입니다.
위로 타고 올라가보면 delegatefrom은 Legacy Token으로 등록이 되어 있고 만약 LagacyToken 컨트랙트에 delegate로 DET가 등록이 되어 있다면 LagacyToken 대신에 DET가 대신 전송될 것 입니다. 저희는 이걸 막기 위해 Dectection Bot을 구현해야 합니다.
Dectection Bot이 Alerts한 숫자들을 비교해 위험이 존재하는지 확인해 트랜잭션을 revert 시키는 구조 입니다.
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");
}
그렇다면 저희가 구현하면 되는 것은 만약 delegateTransfer가 호출이 된다면 Forta의 raiseAlerts를 호출하는 것 입니다.
interface IDetectionBot {
function handleTransaction(address user, bytes calldata msgData) external;
}
contract DetactionBot is IDetectionBot {
Forta forta = Forta(0xA1aE9A5455a4a0CEeF8c816D3a8Df1340460Bbe3);
function handleTransaction(address user, bytes calldata msgData) override external {
forta.raiseAlert(user);
}
}
DetactionBot 컨트랙트는 상당히 간단합니다. 인터페이스를 상속받아 handleTransacion 내부에서 raiseAlert를 호출하면 끝납니다.