// 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;
}
}
이 문제는 cryptovalut가 DET token을 잃는 취약점을 찾고 취약점을 방지해주는 forta bot을 만들면 풀리는 문제이다.
cryptovalut에 token을 빼는 것에 대한 취약점을 찾아야 하기 때문에 관점을 cryptovalut에 DET token을 빼는 공격자 관점으로 바라보자.delegateTransfer에서 _transfer 함수를 호출하는 것이 보인다._transfer() 함수는 transfer() 함수에서 msg.sender를 검증하고 호출하는 함수이다. 즉, unsafe한 함수인 것이다._transfer(address(cryptovalut), ?, address(this).balanceOf(address(cryptovalut))만 실행할 수 있으면 cryptovalut에 토큰을 모두 빼올 수 있을 것이다.delegateTransfer 함수를 중점적으로 분석하면 2개의 modifier가 정의 되어있다. 한번 이를 분석해보자. modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}
msg.sender가 delegateFrom이어야 한다.delegatedFrom = legacyToken;를 볼 수 있다. 즉, legacyToken에서 이를 호출하여야 한다.delegateTransfer를 어떻게 호출할 수 있는지 살펴보자. 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);
}
}
delegateTransfer abi를 발견하였다.delegate가 설정되어 있으면 수행하는데 delegate가 어디로 설정되어있는지 확인해보자.cast call --rpc-url $G_RPC $legacyToken "delegate()"
delegate는 DET token address인 것을 확인할 수 있었다.transfer를 호출하면 DET token에 delegateTransfer() 가 호출되어 onlyDelegateFrom을 통과할 수 있다.msg.sender가 cryptovalut가 되어야한다. cryptovalut를 살펴보자.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)));
}
}
seepToken 함수를 살펴보면 token.transfer()를 수행한다.underlying에 address가 legacy token으로 set 되어있으면 우리는 취약점으로 이어질 수 있다는 것을 알 수 있다.setUnderlying으로 underlying 을 set할 수 있는데 public으로 되어있다.cryptovalut에 모든 DET balance를 뺄 수 있는 취약점을 발견하였다. 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");
}
fortaNotify만 분석하고 detection bot을 구현하면 문제가 풀릴 것이다.notify 함수를 실행하는데 notify에 구현을 보면 우리가 구현해야하는 handleTransaction을 호출한다.botRaisedAlerts는 handleTransaction으로 취약한 tx가 발생할 경우 경고 횟수를 카운트해주는 변수인 것으로 확인되었다.msg.sender가 cryptovalut일 경우에 문제가 발생하기 때문에 이 경우에 botRaisedAlerts를 증가시켜주는 bot만 만들어주면 문제 의도대로 문제가 풀릴 것이다.contract DetectionBot
{
DoubleEntryPoint public target = DoubleEntryPoint(0x6F1a8fc447b9Ea4FE983Da8199d42815AaBd0eef);
function handleTransaction(address user, bytes calldata msgData) external
{
if(bytes4(msgData) == bytes4(target.delegateTransfer.selector))
{
address check;
(,,check) = abi.decode(msgData[4:], (address, uint256, address));
if(check == target.cryptoVault())
{
IForta(msg.sender).raiseAlert(user);
}
}
}
}
이렇게 문제를 해결하였다.