Những ngày gần đây vụ việc bóc phốt giữa ViruSs và Zet khiến mình quan tâm đến dự án game NFT ZukiMoba. Team mình phần lớn là audit và nghiên cứu về các lỗ hổng bảo mật trong lĩnh vực blockchain nên hôm nay mình sẽ đưa ra một số vấn đề trong smartcontracts của dự án ZukiMoba.
Phạm vi bài viết
Về phần Token ZUKI thì ZukiMoba đã audit tại Certik nên mọi người có thể tham khảo tại https://www.certik.com/projects/zukimoba. Trong bài viết này mình sẽ tập trung vào 2 contracts chính:
Để mọi người dễ hiểu thì mình sẽ giới thiệu sơ qua về 2 contracts này. ZUKINFT là token NFT chính của dự án ZukiMoba, trong contract này sẽ chứa các logic về mint, transfer NFT và chứa các thông tin về NFT này như là độ hiếm của, thời gian mint, owner của NFT…
Contract còn lại được viết mục đích để chủ dự án có thể bán NFT cho user và sẽ random sinh ra NFT có độ hiếm dựa vảo tỉ lệ được cấu hình trong contract sau đó sẽ chuyển NFT này đến cho người mua.
Phân tích contracts
ZUKINFT
Đầu tiên mình sẽ đi vào ZUKINFT, có 2 vấn đề mình đánh giá là nghiêm trọng trong contract này. Thứ nhất là việc manager của contract có thể thay đổi được rare của NFT ở hàm changeRare. Ở trong đoạn code dưới thì mọi người có thể thấy owner của contract có thể thay đổi rare của một token bất kỳ nếu muốn. Việc này đi ngược lại tinh thần decentralize của blockchain.
modifier onlySafeNFT() {
require(manager.safeNFT(msg.sender), "require Safe Address.");
_;
}
function changeRare(
uint256 _tokenId,
address _owner,
uint256 _rare
) external override onlySafeNFT {
NFTItem memory nft = nftFactory[_tokenId];
nft.rare = _rare;
User storage userInfo = users[_owner];
for (uint256 index = 0; index < userInfo.nfts.length; index++) {
if(userInfo.nfts[index].tokenId == _tokenId) {
userInfo.nfts[index].rare = _rare;
}
}
emit UpdateRare(_tokenId, _rare);
}
function setManager(
address _addr
) external onlyOwner {
manager = ManagerInterface(_addr);
}
Thứ hai là một lỗi khá cơ bản trong lập trình khiến cho gas của contract trong các hàm transfer sẽ tăng lên đáng kể và có thể tới một thời gian thì chi phí sẽ quá lớn khiến việc thực thi giao dịch là một chuyện bất khả thi.
function _transfer(
address from,
address to,
uint256 tokenId
) internal override(ERC721) {
super._transfer(from, to, tokenId);
User storage userFrom = users[from];
for (uint256 index = 0; index < userFrom.nfts.length; index++) {
if(userFrom.nfts[index].tokenId == tokenId) {
delete userFrom.nfts[index];
}
}
NFTItem memory nftItem = nftFactory[tokenId];
User storage userTo = users[to];
userTo.nfts.push(nftItem);
}
Ở đoạn code trên việc xoá một nft khỏi array không làm giảm kích thước của array và mỗi lần xoá thì phải duyệt hết toàn bộ độ dài của array chứa thông tin. Việc này khiến số lượng gas để thực thi càng ngày càng lớn.
Contract bán NFT
Với những lỗi ở contract NFT thì những vấn đề trên không gây ảnh hưởng hay thiệt hại gì cho dự án và user trong thời gian ngắn nhưng đối với contract bán NFT này thì lại có một lỗi cực kỳ nghiêm trọng mà rất nhiều các dự án game gặp phải khi tham khảo cách sinh ra random từ các nguồn khác sang mà không hiểu rõ bản chất.
Contract không verify source code và điều này khá là ngây thơ khi nghĩ việc giấu source code có thể tránh khỏi sự tấn công của các hacker vì việc decompile sourcecode không quá phức tạp với các hacker.
Mình sẽ ví dụ với hàm BuyDiamond(hàm này nhằm mục đích nhận token từ user và random, mint NFT đến ví của user). Ở hàm này sẽ gọi đến một contract khác ở địa chỉ https://bscscan.com/address/0x2ec3b35c643cb0f45a5163f8c6d0379118a4b99c với mục đích sinh ra một số random nhằm xác định rare của NFT.
def storage:
stor0 is uint256 at storage 0
unknownb9c784b5 is uint256 at storage 1
def unknownb9c784b5() payable:
return unknownb9c784b5
def _fallback() payable: # default function
revert
def unknown191cc088(addr _param1, uint256 _param2) payable:
require ((calldata.size - 4) >= 64)
require ext_code.size(0xe81257d932280ae440b17afc5f07c8a110d21432)
static call 0xe81257d932280ae440b17afc5f07c8a110d21432.balanceOf(address tokenOwner) with:
gas gas_remaining wei
args _param1
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
require (return_data.size >= 32)
require _param2
stor0++
unknownb9c784b5 = (sha3(stor0, block.number, ext_call.return_data[0], eth.balance(_param1), _param1, block.difficulty, block.timestamp) % _param2)
return 0
Ở trên là đoạn code khi decompile của contract random. Mọi người có thể để ý là dự án đang sử dụng các giá trị block.number, balanceOf ZUKI token của tokenOwner, block.difficulty, block.timestamp, … để sinh ra số random nhưng những giá trị này hoàn toán là biết trước được ở trong mạng BSC.
Việc này ảnh hưởng rất nghiêm trọng đến dự án vì hacker có thể kiểm soát và mint ra NFT có độ hiếm giá trị nhất. Rất nhiều game NFT trong quãng thời gian gần đây gặp vấn đề này.
Kết luận
Những lỗi trên sẽ gây ảnh hưởng rất lớn đến dự án và mình mong các dự án sẽ quan tâm và đầu tư hơn về các vấn đề bảo mật của cả ở phần offchain hay trên blockchain. Rất nhiều dự án đã phải chết yểu hay bị tấn công gây ra rất nhiều thiệt hại nghiêm trọng cho cả nhà đầu tư và chủ dự án. Bằng cách audit toàn bộ hệ thống hay ít nhất là toàn bộ smartcontract, các dự án sẽ tránh được các lỗi sơ đẳng trong quá trình phát triển phần mềm của mình. Thân ái 😀.
hay cảm ơn tác giả