// 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);
}
}
}
}
이렇게 문제를 해결하였다.