Inspired by OpenZeppelin's Ethernaut, DoubleEntryPoint Level
This level features a CryptoVault
with special functionality, the sweepToken
function. This is a common function to retrieve tokens stuck in a contract. The CryptoVault
operates with an underlying
token that can't be swept, being it an important core's logic component of the CryptoVault
, any other token can be swept.
The underlying token is an instance of the DET token implemented in DoubleEntryPoint
contract definition and the CryptoVault
holds 100 units of it. Additionally the CryptoVault
also holds 100 of LegacyToken LGT
.
In this level you should figure out where the bug is in CryptoVault
and protect it from being drained out of tokens.
The contract features a Forta
contract where any user can register its own detection bot
contract. Forta is a decentralized, community-based monitoring network to detect threats and anomalies on DeFi, NFT, governance, bridges and other Web3 systems as quickly as possible. Your job is to implement a detection bot
and register it in the Forta
contract. The bot's implementation will need to raise correct alerts to prevent potential attacks or bug exploits.
Hint:
- How does a double entry point work for a token contract?
- Do not care about
sweptTokensRecipient
, focus on preventingDET
transfer fromCryptoVault
.
- Layout of Call Data
- Encoding/Decoding message data
Let's make a watcher bot 🤖
You need to learn more about Forta. But it's out of this game's scope.
ABI Encoder v2 🤔
The Contract Application Binary Interface (ABI) is the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction. Data is encoded according to its type, as described in this specification. The encoding is not self describing and thus requires a schema in order to decode.
We assume the interface functions of a contract are strongly typed, known at compilation time and static. We assume that all contracts will have the interface definitions of any contracts they call available at compile-time.
Understand the encoding rule of function parameters and use this knowledge to get the correct data offset you want to get in calldata
.
i.e. Layout of calldata when function handleTransaction(address user, bytes calldata msgData) external;
is called.
calldata offset | length | element | type | example value |
---|---|---|---|---|
0x00 | 4 | function signature (handleTransaction) | bytes4 | 0x220ab6aa |
0x04 | 32 | user | address | 0x000000000000000000000000XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx |
0x24 | 32 | offset of msgData | uint256 | 0x0000000000000000000000000000000000000000000000000000000000000040 |
0x44 | 32 | length of msgData | uint256 | 0x0000000000000000000000000000000000000000000000000000000000000064 |
0x64 | 4 | function signature (delegateTransfer) | bytes4 | 0x9cd1a121 |
0x68 | 32 | to | address | 0x000000000000000000000000XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx |
0x88 | 32 | value | uint256 | 0x0000000000000000000000000000000000000000000000056bc75e2d63100000 |
0xA8 | 32 | origSender | address | 0x000000000000000000000000XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx |
0xC8 | 28 | padding | bytes | 0x00000000000000000000000000000000000000000000000000000000 |
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/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 {
require(address(usersDetectionBots[msg.sender]) == address(0), "DetectionBot already set");
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) public {
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) public {
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;
}
}
yarn install
yarn compile
yarn test
You should see the result like following:
DetectionBot
√ should set up a detection bot
√ should allow sweep other token
√ should not allow sweep DET token
3 passing (2s)