paint-brush
Khảo sát năm 2023 về bảo mật hợp đồng thông minh cho sự vững chắctừ tác giả@rareskills
6,083 lượt đọc
6,083 lượt đọc

Khảo sát năm 2023 về bảo mật hợp đồng thông minh cho sự vững chắc

từ tác giả RareSkills53m2023/05/16
Read on Terminal Reader

dài quá đọc không nổi

Một vấn đề bảo mật trong Solidity dẫn đến các hợp đồng thông minh không hoạt động theo cách chúng dự kiến. Điều này có thể rơi vào bốn loại chính: Tiền bị đánh cắp Tiền bị khóa hoặc bị đóng băng trong hợp đồng Mọi người nhận được phần thưởng ít hơn dự đoán (phần thưởng bị trì hoãn hoặc giảm) Không thể lập danh sách toàn diện về mọi thứ có thể xảy ra sai sót.
featured image - Khảo sát năm 2023 về bảo mật hợp đồng thông minh cho sự vững chắc
RareSkills HackerNoon profile picture
0-item
1-item
2-item


Danh sách các lỗ hổng của Solidity

Bài viết này phục vụ như một khóa học nhỏ về bảo mật hợp đồng thông minh và cung cấp một danh sách đầy đủ các vấn đề và lỗ hổng bảo mật có xu hướng tái diễn trong các hợp đồng thông minh của Solidity. Đây là những loại vấn đề có thể phát sinh trong một cuộc kiểm toán chất lượng.


Một vấn đề bảo mật trong Solidity dẫn đến các hợp đồng thông minh không hoạt động theo cách chúng dự kiến.


Điều này có thể rơi vào bốn loại lớn:

  • Tiền bị đánh cắp

  • Tiền bị khóa hoặc đóng băng trong hợp đồng

  • Mọi người nhận được ít phần thưởng hơn dự đoán (phần thưởng bị trì hoãn hoặc giảm)

  • Mọi người nhận được nhiều phần thưởng hơn dự đoán (dẫn đến lạm phát và mất giá)


Không thể lập một danh sách toàn diện về mọi thứ có thể sai. Tuy nhiên, giống như công nghệ phần mềm truyền thống có các chủ đề phổ biến về các lỗ hổng như SQL injection, lỗi tràn bộ đệm và tập lệnh chéo trang, các hợp đồng thông minh có các mẫu chống định kỳ có thể được ghi lại.


Hãy nghĩ về hướng dẫn này như một tài liệu tham khảo. Không thể thảo luận chi tiết về mọi khái niệm mà không biến nó thành một cuốn sách (cảnh báo công bằng: bài viết này dài hơn 10 nghìn từ, vì vậy hãy đánh dấu trang và đọc nó theo từng phần). Tuy nhiên, nó phục vụ như một danh sách những gì cần chú ý và những gì cần nghiên cứu. Nếu một chủ đề cảm thấy không quen thuộc, thì đó sẽ là dấu hiệu cho thấy đáng để dành thời gian thực hành xác định loại lỗ hổng đó.

điều kiện tiên quyết

Bài viết này giả định trình độ cơ bản trong Solidity . Nếu bạn chưa quen với Solidity, vui lòng xem hướng dẫn Solidity miễn phí của chúng tôi.

tái nhập cư

Chúng tôi đã viết rất nhiều về việc tham gia lại hợp đồng thông minh nên chúng tôi sẽ không nhắc lại ở đây. Nhưng đây là một bản tóm tắt nhanh chóng:


Bất cứ khi nào một hợp đồng thông minh gọi chức năng của một hợp đồng thông minh khác, gửi Ether đến nó hoặc chuyển mã thông báo cho nó, thì sẽ có khả năng tái đăng ký.


  • Khi Ether được chuyển, chức năng nhận hoặc dự phòng của hợp đồng nhận được gọi. Điều này trao quyền kiểm soát cho người nhận.
  • Một số giao thức mã thông báo cảnh báo hợp đồng thông minh nhận rằng họ đã nhận được mã thông báo bằng cách gọi một chức năng được xác định trước. Điều này trao luồng điều khiển cho chức năng đó.
  • Khi một hợp đồng tấn công nhận được quyền kiểm soát, nó không cần phải gọi cùng chức năng đã chuyển giao quyền kiểm soát. Nó có thể gọi một chức năng khác trong hợp đồng thông minh của nạn nhân (tham gia lại chức năng chéo) hoặc thậm chí là một hợp đồng khác (tham gia lại hợp đồng chéo)
  • Reentrancy chỉ đọc xảy ra khi chức năng xem được truy cập trong khi hợp đồng ở trạng thái trung gian.


Mặc dù reentrancy có thể là lỗ hổng hợp đồng thông minh nổi tiếng nhất, nhưng nó chỉ chiếm một tỷ lệ nhỏ các vụ hack xảy ra trong tự nhiên. Nhà nghiên cứu bảo mật Pascal Caversaccio (pcaveraccio) luôn cập nhật danh sách github về các cuộc tấn công vào lại . Kể từ tháng 4 năm 2023, 46 cuộc tấn công vào lại đã được ghi lại trong kho lưu trữ đó.

Kiểm soát truy cập

Nó có vẻ như là một lỗi đơn giản, nhưng việc quên đặt giới hạn đối với những người có thể gọi một chức năng nhạy cảm (như rút ether hoặc thay đổi quyền sở hữu) xảy ra thường xuyên một cách đáng ngạc nhiên.


Ngay cả khi đã có công cụ sửa đổi, vẫn có trường hợp công cụ sửa đổi không được triển khai chính xác, chẳng hạn như trong ví dụ bên dưới khi thiếu câu lệnh yêu cầu.

 // DO NOT USE! modifier onlyMinter { minters[msg.sender] == true_; }

Đoạn mã trên là một ví dụ thực tế từ cuộc kiểm toán này: https://code4rena.com/reports/2023-01-rabbithole/#h-01-bad-implementation-in-minter-access-control-for-rabbitholereceipt-and- hợp đồng thỏholetickets


Đây là một cách khác kiểm soát truy cập có thể đi sai

 function claimAirdrop(bytes32 calldata proof[]) { bool verified = MerkleProof.verifyCalldata(proof, merkleRoot, keccak256(abi.encode(msg.sender))); require(verified, "not verified"); require(alreadyClaimed[msg.sender], "already claimed"); _transfer(msg.sender, AIRDROP_AMOUNT); }

Trong trường hợp này, “alreadyClaimed” không bao giờ được đặt thành true, vì vậy người xác nhận quyền sở hữu có thể gọi hàm này nhiều lần.

Ví dụ thực tế: Bot thương nhân bị khai thác

Một ví dụ khá gần đây về việc kiểm soát truy cập không đầy đủ là một chức năng không được bảo vệ để nhận các khoản vay chớp nhoáng bởi một bot giao dịch (có tên là 0xbad, vì địa chỉ bắt đầu bằng chuỗi đó). Nó đã kiếm được hơn một triệu đô la lợi nhuận cho đến một ngày kẻ tấn công nhận thấy bất kỳ địa chỉ nào cũng có thể gọi chức năng nhận flashloan, không chỉ nhà cung cấp flashloan.


Như thường xảy ra với các bot giao dịch, mã hợp đồng thông minh để thực hiện các giao dịch không được xác minh, nhưng kẻ tấn công vẫn phát hiện ra điểm yếu. Thông tin thêm trong tin tức rekt .

Xác thực đầu vào không đúng cách

Nếu kiểm soát truy cập là kiểm soát ai gọi một chức năng, thì xác thực đầu vào là kiểm soát những gì họ gọi hợp đồng.


Điều này thường dẫn đến việc quên đặt các câu lệnh yêu cầu thích hợp.

Đây là một ví dụ thô sơ:

 contract UnsafeBank { mapping(address => uint256) public balances; // allow depositing on other's behalf function deposit(address for) public payable { balances += msg.value; } function withdraw(address from, uint256 amount) public { require(balances[from] <= amount, "insufficient balance"); balances[from] -= amount; msg.sender.call{value: amout}(""); } }

Hợp đồng ở trên kiểm tra xem bạn có rút nhiều hơn số tiền bạn có trong tài khoản của mình hay không, nhưng nó không ngăn bạn rút tiền từ một tài khoản tùy ý.

Ví dụ thực tế: Sushiswap

Sushiswap đã gặp phải một vụ hack kiểu này do một trong các tham số của chức năng bên ngoài không được vệ sinh.

hack Sushiswap

Sự khác biệt giữa kiểm soát truy cập không đúng cách và xác thực đầu vào không đúng cách là gì?

Kiểm soát truy cập không phù hợp có nghĩa là msg.sender không có các hạn chế thích hợp. Xác thực đầu vào không đúng cách có nghĩa là các đối số của hàm không được làm sạch đầy đủ. Ngoài ra còn có một nghịch đảo đối với mô hình chống đối này: đặt quá nhiều hạn chế cho một lệnh gọi hàm.

Hạn chế chức năng quá mức

Xác thực quá mức có thể có nghĩa là tiền sẽ không bị đánh cắp, nhưng điều đó có thể có nghĩa là tiền bị khóa trong hợp đồng. Có quá nhiều biện pháp bảo vệ tại chỗ cũng không phải là một điều tốt.

Ví dụ thực tế: Akutars NFT

Một trong những sự cố nổi tiếng nhất là Akutars NFT kết thúc với số Eth trị giá 34 triệu đô la bị mắc kẹt trong hợp đồng thông minh và không thể rút được.


Hợp đồng có một cơ chế có mục đích tốt để ngăn chủ sở hữu hợp đồng rút lại cho đến khi tất cả các khoản hoàn trả từ việc thanh toán trên giá đấu giá của Hà Lan đã được đưa ra. Nhưng do một lỗi được ghi lại trong chuỗi Twitter được liên kết bên dưới, chủ sở hữu đã không thể rút tiền.

Lỗ hổng Akutars NFT

Nhận được sự cân bằng ngay

Sushiswap trao quá nhiều quyền cho những người dùng không đáng tin cậy và Akutars NFT trao quá ít quyền cho quản trị viên. Khi thiết kế các hợp đồng thông minh, một đánh giá chủ quan về mức độ tự do mà mỗi loại người dùng phải được thực hiện và quyết định này không thể để cho công cụ và thử nghiệm tự động. Có những sự đánh đổi đáng kể với tính phi tập trung, bảo mật và UX phải được xem xét.


Đối với lập trình viên hợp đồng thông minh, viết rõ ràng những gì người dùng nên và không thể làm với một số chức năng nhất định là một phần quan trọng của quá trình phát triển.

Chúng ta sẽ xem lại chủ đề về các quản trị viên bị áp đảo sau.

Bảo mật thường tập trung vào việc quản lý cách tiền thoát khỏi hợp đồng

Như đã nêu trong phần giới thiệu, có bốn cách chính để hợp đồng thông minh bị hack:


  • tiền bị đánh cắp
  • tiền đóng băng
  • Phần thưởng không đủ
  • Phần thưởng quá mức


“Tiền” ở đây có nghĩa là bất cứ thứ gì có giá trị, chẳng hạn như mã thông báo, không chỉ tiền điện tử. Khi mã hóa hoặc kiểm tra một hợp đồng thông minh, nhà phát triển phải có ý thức về các cách dự định mà giá trị sẽ chảy vào và ra khỏi hợp đồng. Các vấn đề được liệt kê ở trên là những cách chính mà hợp đồng thông minh bị tấn công, nhưng có rất nhiều nguyên nhân gốc rễ khác có thể dẫn đến các vấn đề lớn, được ghi lại bên dưới.

Bỏ phiếu kép hoặc giả mạo msg.sender

Việc sử dụng mã thông báo ERC20 cố định hoặc NFT làm vé để cân phiếu bầu là không an toàn vì kẻ tấn công có thể bỏ phiếu bằng một địa chỉ, chuyển mã thông báo sang địa chỉ khác và bỏ phiếu lại từ địa chỉ đó.

Đây là một ví dụ tối thiểu:

 // A malicious voter can simply transfer their tokens to // another address and vote again. contract UnsafeBallot { uint256 public proposal1VoteCount; uint256 public proposal2VoteCount; IERC20 immutable private governanceToken; constructor(IERC20 _governanceToken) { governanceToken = _governanceToken; } function voteFor1() external notAlreadyVoted { proposal1VoteCount += governanceToken.balanceOf(msg.sender); } function voteFor2() external notAlreadyVoted { proposal2VoteCount += governanceToken.balanceOf(msg.sender); } // prevent the same address from voting twice, // however the attacker can simply // transfer to a new address modifier notAlreadyVoted { require(!alreadyVoted[msg.sender], "already voted"); _; alreadyVoted[msg.sender] = true; } }

Để ngăn chặn cuộc tấn công này, nên sử dụng Ảnh chụp nhanh ERC20 hoặc Phiếu bầu ERC20 . Bằng cách chụp nhanh một thời điểm trong quá khứ, số dư mã thông báo hiện tại không thể bị thao túng để có được quyền biểu quyết bất hợp pháp.

Tấn công quản trị Flashloan

Tuy nhiên, việc sử dụng mã thông báo ERC20 với ảnh chụp nhanh hoặc khả năng bỏ phiếu không giải quyết được hoàn toàn vấn đề nếu ai đó có thể vay nhanh để tạm thời tăng số dư của họ, sau đó chụp nhanh số dư của họ trong cùng một giao dịch. Nếu ảnh chụp nhanh đó được sử dụng để bỏ phiếu, họ sẽ có một lượng phiếu bầu lớn bất hợp lý tùy ý sử dụng.


Khoản vay chớp nhoáng cho một địa chỉ vay một lượng lớn Ether hoặc mã thông báo, nhưng sẽ hoàn nguyên nếu số tiền này không được hoàn trả trong cùng một giao dịch.

 contract SimpleFlashloan { function borrowERC20Tokens() public { uint256 before = token.balanceOf(address(this)); // send tokens to the borrower token.transfer(msg.sender, amount); // hand control back to the borrower to // let them do something IBorrower(msg.sender).onFlashLoan(); // require that the tokens got returned require(token.balanceOf(address(this) >= before); } }

Kẻ tấn công có thể sử dụng flashloan để đột nhiên giành được nhiều phiếu bầu nhằm đưa ra các đề xuất có lợi cho chúng và/hoặc làm điều gì đó ác ý.

Tấn công giá Flashloan

Đây được cho là cuộc tấn công phổ biến nhất (hoặc ít nhất là nghiêm trọng nhất) vào DeFi, gây thiệt hại hàng trăm triệu đô la. Dưới đây là danh sách những hồ sơ cao.


Giá của một tài sản trên chuỗi khối thường được tính bằng tỷ giá hối đoái hiện tại giữa các tài sản. Ví dụ: nếu một hợp đồng hiện đang giao dịch 1 USDC với 100 k9coin, thì bạn có thể nói k9coin có giá là 0,01 USDC. Tuy nhiên, giá thường di chuyển để đối phó với áp lực mua và bán, và các khoản vay chớp nhoáng có thể tạo ra áp lực mua và bán lớn.


Khi truy vấn một hợp đồng thông minh khác về giá của một tài sản, nhà phát triển cần phải rất cẩn thận vì họ cho rằng hợp đồng thông minh mà họ đang gọi không bị thao túng khoản vay chớp nhoáng.

Bỏ qua việc kiểm tra hợp đồng

Bạn có thể “kiểm tra” xem địa chỉ có phải là hợp đồng thông minh hay không bằng cách xem kích thước mã byte của địa chỉ đó. Tài khoản thuộc sở hữu bên ngoài (ví thông thường) không có bất kỳ mã byte nào. Dưới đây là một vài cách để làm điều đó

 import "@openzeppelin/contracts/utils/Address.sol" contract CheckIfContract { using Address for address; function addressIsContractV1(address _a) { return _a.code.length == 0; } function addressIsContractV2(address _a) { // use the openzeppelin libraryreturn _a.isContract(); } }


Tuy nhiên, điều này có một số hạn chế

  • Nếu một hợp đồng thực hiện lệnh gọi bên ngoài từ một hàm tạo, thì kích thước mã byte rõ ràng của nó sẽ bằng 0 vì mã triển khai hợp đồng thông minh chưa trả về mã thời gian chạy
  • Hiện tại không gian có thể trống, nhưng kẻ tấn công có thể biết rằng họ có thể triển khai hợp đồng thông minh ở đó trong tương lai bằng cách sử dụng create2


Nói chung, việc kiểm tra xem một địa chỉ có phải là hợp đồng hay không thường (nhưng không phải luôn luôn) là một phản mẫu. Bản thân ví đa chữ ký là hợp đồng thông minh và làm bất cứ điều gì có thể phá vỡ ví đa chữ ký sẽ phá vỡ khả năng kết hợp.


Ngoại lệ đối với điều này là kiểm tra xem mục tiêu có phải là hợp đồng thông minh hay không trước khi gọi hook chuyển nhượng. Thêm về điều này sau.

tx.origin

Hiếm khi có lý do chính đáng để sử dụng tx.origin. Nếu tx.origin được sử dụng để xác định người gửi, thì có thể xảy ra một cuộc tấn công trung gian. Nếu người dùng bị lừa gọi một hợp đồng thông minh độc hại, thì hợp đồng thông minh đó có thể sử dụng tất cả quyền hạn mà tx.origin có để tàn phá.


Xem xét bài tập sau đây và các nhận xét phía trên đoạn mã.

 contract Phish { function phishingFunction() public { // this fails, because this contract does not have approval/allowance token.transferFrom(msg.sender, address(this), token.balanceOf(msg.sender)); // this also fails, because this creates approval for the contract,// not the wallet calling this phishing function token.approve(address(this), type(uint256).max); } }


Điều này không có nghĩa là bạn an toàn khi gọi các hợp đồng thông minh tùy ý. Nhưng có một lớp an toàn được tích hợp trong hầu hết các giao thức sẽ bị bỏ qua nếu tx.origin được sử dụng để xác thực.

Đôi khi, bạn có thể thấy mã giống như sau:

 require(msg.sender == tx.origin, "no contracts");


Khi một hợp đồng thông minh gọi một hợp đồng thông minh khác, msg.sender sẽ là hợp đồng thông minh và tx.origin sẽ là ví của người dùng, do đó đưa ra một dấu hiệu đáng tin cậy rằng cuộc gọi đến là từ một hợp đồng thông minh. Điều này đúng ngay cả khi cuộc gọi xảy ra từ hàm tạo.


Hầu hết thời gian, mẫu thiết kế này không phải là một ý tưởng hay. Ví đa chữ ký và Ví từ EIP 4337 sẽ không thể tương tác với chức năng có mã này. Mô hình này thường có thể được nhìn thấy trong các loại tiền đúc NFT, nơi hợp lý để mong đợi hầu hết người dùng đang sử dụng ví truyền thống. Nhưng khi tính trừu tượng của tài khoản trở nên phổ biến hơn, mô hình này sẽ cản trở nhiều hơn là hữu ích.

Gas Đau buồn hoặc từ chối dịch vụ

Một cuộc tấn công đau buồn có nghĩa là tin tặc đang cố gắng "gây đau buồn" cho người khác, ngay cả khi họ không thu được lợi ích kinh tế từ việc đó.


Một hợp đồng thông minh có thể sử dụng hết gas được chuyển tiếp đến nó một cách ác ý bằng cách đi vào một vòng lặp vô hạn. Hãy xem xét ví dụ sau:

 contract Mal { fallback() external payable { // infinite loop uses up all the gas while (true) { } } }


Nếu một hợp đồng khác phân phối ether cho một danh sách các địa chỉ như sau:

 contract Distribute { funtion distribute(uint256 total) public nonReentrant { for (uint i; i < addresses.length; ) { (bool ok, ) addresses.call{value: total / addresses.length}(""); // ignore ok, if it reverts we move on // traditional gas saving trick for for loops unchecked { ++i; } } } }


Sau đó, chức năng sẽ hoàn nguyên khi gửi ether tới Mal. Cuộc gọi trong mã ở trên chuyển tiếp 63/64 lượng gas có sẵn, vì vậy có khả năng sẽ không có đủ gas để hoàn thành hoạt động khi chỉ còn 1/64 lượng gas.


Một hợp đồng thông minh có thể trả về một mảng bộ nhớ lớn tiêu tốn nhiều gas

Xem xét ví dụ sau

 function largeReturn() public { // result might be extremely long! (book ok, bytes memory result) = otherContract.call(abi.encodeWithSignature("foo()")); require(ok, "call failed"); }

Mảng bộ nhớ sử dụng hết lượng gas bậc hai sau 724 byte, do đó, kích thước dữ liệu trả về được chọn cẩn thận có thể khiến người gọi khó chịu.


Ngay cả khi biến result không được sử dụng, nó vẫn được sao chép vào bộ nhớ. Nếu bạn muốn hạn chế kích thước trả về ở một lượng nhất định, bạn có thể sử dụng hợp ngữ

 function largeReturn() public { assembly { let ok := call(gas(), destinationAddress, value, dataOffset, dataSize, 0x00, 0x00); // nothing is copied to memory until you // use returndatacopy() } }

Xóa các mảng mà người khác có thể thêm vào cũng là một vectơ từ chối dịch vụ

Mặc dù xóa bộ lưu trữ là một hoạt động tiết kiệm gas, nhưng nó vẫn có chi phí ròng. Nếu một mảng trở nên quá dài, nó sẽ không thể xóa được. Đây là một ví dụ tối thiểu

 contract VulnerableArray { address[] public stuff; function addSomething(address something) public { stuff.push(something); } // if stuff is too long, this will become undeletable due to // the gas cost function deleteEverything() public onlyOwner { delete stuff; } }

ERC777, ERC721 và ERC1155 cũng có thể là vectơ đau buồn

Nếu hợp đồng thông minh chuyển mã thông báo có móc chuyển, kẻ tấn công có thể thiết lập hợp đồng không chấp nhận mã thông báo (hợp đồng này không có chức năng onReceive hoặc lập trình chức năng để hoàn nguyên). Điều này sẽ làm cho mã thông báo không thể chuyển nhượng được và khiến toàn bộ giao dịch bị hoàn nguyên.


Trước khi sử dụng SafeTransfer hoặc chuyển khoản, hãy xem xét khả năng người nhận có thể buộc hoàn nguyên giao dịch.

 contract Mal is IERC721Receiver, IERC1155Receiver, IERC777Receiver { // this will intercept any transfer hook fallback() external payable { // infinite loop uses up all the gaswhile (true) { } } // we could also selectively deny transactions function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { if (wakeUpChooseViolence()) { revert(); } else { return IERC721Receiver.onERC721Received.selector; } } }

Tính ngẫu nhiên không an toàn

Hiện tại không thể tạo ngẫu nhiên một cách an toàn với một giao dịch duy nhất trên chuỗi khối. Các chuỗi khối cần phải hoàn toàn xác định, nếu không các nút phân tán sẽ không thể đạt được sự đồng thuận về trạng thái. Bởi vì chúng hoàn toàn mang tính xác định, bất kỳ số "ngẫu nhiên" nào cũng có thể được dự đoán. Chức năng tung xúc xắc sau đây có thể được khai thác.


 contract UnsafeDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } // our dice can land on one of {0,1,2,3,4,5}function rollDice() public payable { require(msg.value == 1 ether); if (randomness() % 6) == 5) { msg.sender.call{value: 2 ether}(""); } } } contract ExploitDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } function betSafely(IUnsafeDice game) public payable { if (randomness % 6) == 5)) { game.betSafely{value: 1 ether}() } // else don't do anything } }


Việc bạn tạo ra tính ngẫu nhiên như thế nào không quan trọng vì kẻ tấn công có thể sao chép nó một cách chính xác. Đưa vào nhiều nguồn “entropy” hơn như msg.sender, dấu thời gian, v.v. sẽ không có bất kỳ tác dụng nào vì hợp đồng thông minh có thể đo lường hai nguồn đó.

Sử dụng tính ngẫu nhiên của Chainlink Oracle đã sai

Chainlink là một giải pháp phổ biến để nhận các số ngẫu nhiên an toàn. Nó làm điều đó trong hai bước. Đầu tiên, các hợp đồng thông minh gửi một yêu cầu ngẫu nhiên đến nhà tiên tri, sau đó một số khối sau đó, nhà tiên tri sẽ phản hồi bằng một số ngẫu nhiên.


Vì kẻ tấn công không thể dự đoán tương lai nên chúng không thể dự đoán số ngẫu nhiên.

Trừ khi hợp đồng thông minh sử dụng oracle sai.


  • Hợp đồng thông minh yêu cầu tính ngẫu nhiên không được làm bất cứ điều gì cho đến khi số ngẫu nhiên được trả về. Mặt khác, kẻ tấn công có thể theo dõi mempool để biết tiên tri trả về tính ngẫu nhiên và chạy trước tiên tri, biết số ngẫu nhiên sẽ là bao nhiêu.
  • Bản thân các nhà tiên tri ngẫu nhiên có thể cố gắng thao túng ứng dụng của bạn. Chúng không thể chọn các số ngẫu nhiên mà không có sự đồng thuận từ các nút khác, nhưng chúng có thể giữ lại và sắp xếp lại các số ngẫu nhiên nếu ứng dụng của bạn yêu cầu nhiều số cùng một lúc.
  • Tính hữu hạn không phải là ngay lập tức trên Ethereum hoặc hầu hết các chuỗi EVM khác. Chỉ vì một số khối là khối gần đây nhất, điều đó không có nghĩa là nó sẽ không nhất thiết phải giữ nguyên như vậy. Điều này được gọi là “tái tổ chức chuỗi”. Trên thực tế, chuỗi có thể thay đổi nhiều hơn chỉ khối cuối cùng. Đây được gọi là “độ sâu tổ chức lại”. Etherscan báo cáo tổ chức lại cho các chuỗi khác nhau, ví dụ như tổ chức lại Ethereum và tổ chức lại Polygon. Các tổ chức lại có thể sâu tới 30 khối trở lên trên Đa giác, do đó, việc chờ đợi ít khối hơn có thể khiến ứng dụng dễ bị tổn thương (điều này có thể thay đổi khi zk-evm trở thành sự đồng thuận tiêu chuẩn trên Đa giác, vì tính hữu hạn sẽ khớp với Ethereum nhưng đây là dự đoán trong tương lai , không phải là một sự thật về hiện tại).

Lấy dữ liệu cũ từ giá Oracle

Không có SLA (thỏa thuận cấp độ dịch vụ) cho Chainlink để giữ cho giá của nó được cập nhật trong một khung thời gian nhất định. Khi chuỗi bị tắc nghẽn nghiêm trọng (chẳng hạn như khi Yuga Labs Otherside đúc Ethereum áp đảo đến mức không có giao dịch nào được thực hiện), việc cập nhật giá có thể bị trì hoãn.


Một hợp đồng thông minh sử dụng tiên tri giá phải kiểm tra rõ ràng dữ liệu không cũ, tức là đã được cập nhật gần đây trong một số ngưỡng. Mặt khác, nó không thể đưa ra quyết định đáng tin cậy về giá cả.


Có một điều phức tạp nữa là nếu giá không thay đổi vượt qua ngưỡng sai lệch , nhà tiên tri có thể không cập nhật giá để tiết kiệm gas, vì vậy, điều này có thể ảnh hưởng đến ngưỡng thời gian nào được coi là “cũ”.


Điều quan trọng là phải hiểu SLA của một tiên tri mà hợp đồng thông minh dựa vào.

Chỉ dựa vào một lời tiên tri

Bất kể lời tiên tri có vẻ an toàn đến đâu, một cuộc tấn công có thể bị phát hiện trong tương lai. Cách phòng thủ duy nhất chống lại điều này là sử dụng nhiều lời tiên tri độc lập.

Oracles nói chung là khó để có được đúng

Chuỗi khối có thể khá an toàn, nhưng việc đưa dữ liệu vào chuỗi ngay từ đầu đòi hỏi một số loại hoạt động ngoài chuỗi mà bỏ qua tất cả các đảm bảo an ninh mà chuỗi khối cung cấp. Ngay cả khi các nhà tiên tri vẫn trung thực, nguồn dữ liệu của họ vẫn có thể bị thao túng. Ví dụ: một nhà tiên tri có thể báo cáo giá một cách đáng tin cậy từ một sàn giao dịch tập trung, nhưng những giá đó có thể bị thao túng với các lệnh mua và bán lớn. Tương tự như vậy, các nhà tiên tri phụ thuộc vào dữ liệu cảm biến hoặc một số API web2 phải tuân theo các hướng tấn công truyền thống.


Một kiến trúc hợp đồng thông minh tốt sẽ tránh hoàn toàn việc sử dụng các oracle nếu có thể.

kế toán hỗn hợp

Xét hợp đồng sau

 contract MixedAccounting { uint256 myBalance; function deposit() public payable { myBalance = myBalance + msg.value; } function myBalanceIntrospect() public view returns (uint256) { return address(this).balance; } function myBalanceVariable() public view returns (uint256) { return myBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }


Hợp đồng ở trên không có chức năng nhận hoặc dự phòng, do đó, chuyển trực tiếp Ether sang nó sẽ hoàn nguyên. Tuy nhiên, một hợp đồng có thể buộc gửi Ether đến nó bằng cách tự hủy.


Trong trường hợp đó, myBalanceIntrospect() sẽ lớn hơn myBalanceVariable(). Phương pháp kế toán Ether là tốt, nhưng nếu bạn sử dụng cả hai, thì hợp đồng có thể có hành vi không nhất quán.


Điều tương tự cũng áp dụng cho mã thông báo ERC20.

 contract MixedAccountingERC20 { IERC20 token; uint256 myTokenBalance; function deposit(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); myTokenBalance = myTokenBalance + amount; } function myBalanceIntrospect() public view returns (uint256) { return token.balanceOf(address(this)); } function myBalanceVariable() public view returns (uint256) { return myTokenBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }

Một lần nữa, chúng ta không thể cho rằng myBalanceIntrospect() và myBalanceVariable() sẽ luôn trả về cùng một giá trị. Có thể chuyển trực tiếp mã thông báo ERC20 sang MixedAccountingERC20, bỏ qua chức năng gửi tiền và không cập nhật biến myTokenBalance.


Khi kiểm tra số dư bằng nội quan, nên tránh kiểm tra nghiêm ngặt bằng cách sử dụng số dư vì số dư có thể bị thay đổi bởi người ngoài theo ý muốn.

Xử lý bằng chứng mật mã như mật khẩu

Đây không phải là một sự châm biếm của Solidity, mà là một sự hiểu lầm phổ biến hơn giữa các nhà phát triển về cách sử dụng mật mã để cung cấp các đặc quyền đặc biệt cho địa chỉ. Đoạn mã sau không an toàn

 contract InsecureMerkleRoot { bytes32 merkleRoot; function airdrop(bytes[] calldata proof, bytes32 leaf) external { require(MerkleProof.verifyCalldata(proof, merkleRoot, leaf), "not verified"); require(!alreadyClaimed[leaf], "already claimed airdrop"); alreadyClaimed[leaf] = true; mint(msg.sender, AIRDROP_AMOUNT); } }


Mã này không an toàn vì ba lý do:

  1. Bất kỳ ai biết địa chỉ được chọn cho airdrop đều có thể tạo lại cây merkle và tạo bằng chứng hợp lệ.
  2. Lá không được băm. Kẻ tấn công có thể gửi một lá bằng gốc merkle và bỏ qua câu lệnh yêu cầu.
  3. Ngay cả khi hai vấn đề trên đã được khắc phục, thì một khi ai đó gửi bằng chứng hợp lệ, họ có thể bị xử lý trước.


Bằng chứng mật mã (cây merkle, chữ ký, v.v.) cần phải được liên kết với msg.sender, kẻ tấn công không thể thao tác nếu không lấy được khóa riêng.

Solidity không upcast đến kích thước uint cuối cùng

 function limitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = a * b; }

Mặc dù sản phẩm là biến uint256 nhưng kết quả phép nhân không được lớn hơn 255, nếu không mã sẽ hoàn nguyên.


Vấn đề này có thể được giảm thiểu bằng cách upcasting riêng từng biến.

 function unlimitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = uint256(a) * uint256(b); }

Một tình huống như thế này có thể xảy ra nếu nhân các số nguyên được đóng gói trong một cấu trúc. Bạn nên lưu ý điều này khi nhân các giá trị nhỏ được đóng gói trong một cấu trúc

 struct Packed { uint8 time; uint16 rewardRate } //... Packed p; p.time * p.rewardRate; // this might revert!

Solidity downcasting không hoàn nguyên khi tràn

Solidity không kiểm tra xem có an toàn khi truyền một số nguyên thành một số nhỏ hơn hay không. Trừ khi một số logic nghiệp vụ đảm bảo rằng quá trình truyền xuống là an toàn, nên sử dụng một thư viện như SafeCast .

 function test(int256 value) public pure returns (int8) { return int8(value + 1); // overflows and does not revert }

Ghi vào con trỏ lưu trữ không lưu dữ liệu mới.

Có vẻ như mã này sao chép dữ liệu trong myArray[1] sang myArray[0], nhưng thực tế không phải vậy. Nếu bạn nhận xét dòng cuối cùng trong hàm, trình biên dịch sẽ cho biết hàm này sẽ được chuyển thành hàm xem. Việc ghi vào foo không ghi vào bộ nhớ cơ bản.


 contract DoesNotWrite { struct Foo { uint256 bar; } Foo[] public myArray; function moveToSlot0() external { Foo storage foo = myArray[0]; foo = myArray[1]; // myArray[0] is unchanged // we do this to make the function a state // changing operation // and silence the compiler warning myArray[1] = Foo({bar: 100}); } }

Vì vậy, đừng ghi vào con trỏ lưu trữ.

Xóa cấu trúc chứa kiểu dữ liệu động không xóa dữ liệu động

Nếu một ánh xạ (hoặc mảng động) nằm bên trong một cấu trúc và cấu trúc đó bị xóa, thì ánh xạ hoặc mảng đó sẽ không bị xóa.


Ngoại trừ việc xóa một mảng, từ khóa xóa chỉ có thể xóa một khe lưu trữ. Nếu vị trí lưu trữ chứa tham chiếu đến các vị trí lưu trữ khác, thì những vị trí đó sẽ không bị xóa.

 contract NestedDelete { mapping(uint256 => Foo) buzz; struct Foo { mapping(uint256 => uint256) bar; } Foo foo; function addToFoo(uint256 i) external { buzz[i].bar[5] = 6; } function getFromFoo(uint256 i) external view returns (uint256) { return buzz[i].bar[5]; } function deleteFoo(uint256 i) external { // internal map still holds the data in the // mapping and array delete buzz[i]; } }


Bây giờ hãy thực hiện chuỗi giao dịch sau

  1. thêmToFoo(1)
  2. getFromFoo(1) trả về 6
  3. xóaFoo(1)
  4. getFromFoo(1) vẫn trả về 6!


Hãy nhớ rằng, bản đồ không bao giờ “trống rỗng” trong Solidity. Vì vậy, nếu ai đó truy cập vào một mục đã bị xóa, giao dịch sẽ không hoàn nguyên mà thay vào đó trả về giá trị 0 cho kiểu dữ liệu đó.

Các vấn đề về mã thông báo ERC20

Nếu bạn chỉ xử lý các mã thông báo ERC20 đáng tin cậy, hầu hết các vấn đề này sẽ không xảy ra. Tuy nhiên, khi tương tác với mã thông báo ERC20 tùy ý hoặc không đáng tin cậy một phần, đây là một số điều cần chú ý.

ERC20: Phí chuyển tiền

Khi xử lý các mã thông báo không đáng tin cậy, bạn không nên cho rằng số dư của mình nhất thiết phải tăng theo số lượng. Mã thông báo ERC20 có thể triển khai chức năng chuyển của nó như sau:

 contract ERC20 { // internally called by transfer() and transferFrom() // balance and approval checks happen in the caller function _transfer(address from, address to, uint256 amount) internal returns (bool) { fee = amount * 100 / 99; balanceOf[from] -= to; balanceOf[to] += (amount - fee); balanceOf[TREASURY] += fee; emit Transfer(msg.sender, to, (amount - fee)); return true; } }


Mã thông báo này áp dụng thuế 1% cho mọi giao dịch. Vì vậy, nếu một hợp đồng thông minh tương tác với mã thông báo như sau, chúng tôi sẽ nhận được tiền hoàn lại không mong muốn hoặc tiền bị đánh cắp.

 contract Stake { mapping(address => uint256) public balancesInContract; function stake(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); balancesInContract[msg.sender] += amount; // THIS IS WRONG! } function unstake() public { uint256 toSend = balancesInContract[msg.sender]; delete balancesInContract[msg.sender]; // this could revert because toSend is 1% greater than// the amount in the contract. Otherwise, 1% will be "stolen"// from other depositors. token.transfer(msg.sender, toSend); } }

ERC20: khởi động lại mã thông báo

Mã thông báo rebasing đã được phổ biến bởi mã thông báo sOhm của Olympus DAO và mã thông báo AMPL của Ampleforth . Coingecko duy trì một danh sách các mã thông báo ERC20 đang khởi động lại.


Khi một mã thông báo bị rebase, tổng nguồn cung sẽ thay đổi và số dư của mọi người tăng hoặc giảm tùy thuộc vào hướng rebase.


Đoạn mã sau có khả năng bị hỏng khi xử lý mã thông báo khởi động lại

 contract WillBreak { mapping(address => uint256) public balanceHeld; IERC20 private rebasingToken function deposit(uint256 amount) external { balanceHeld[msg.sender] = amount; rebasingToken.transferFrom(msg.sender, address(this), amount); } function withdraw() external { amount = balanceHeld[msg.sender]; delete balanceHeld[msg.sender]; // ERROR, amount might exceed the amount // actually held by the contract rebasingToken.transfer(msg.sender, amount); } }


Giải pháp của nhiều hợp đồng đơn giản là không cho phép rebasing token. Tuy nhiên, người ta có thể sửa đổi mã ở trên để kiểm tra balanceOf(address(this)) trước khi chuyển số dư tài khoản cho người gửi. Sau đó, nó sẽ vẫn hoạt động ngay cả khi số dư thay đổi.

ERC20: ERC777 trong quần áo ERC20

ERC20, nếu được triển khai theo tiêu chuẩn, mã thông báo ERC20 không có móc chuyển và do đó, chuyển và chuyểnTừ không có vấn đề về đăng nhập lại.


Có những lợi thế đáng kể đối với mã thông báo có móc chuyển, đó là lý do tại sao tất cả các tiêu chuẩn NFT triển khai chúng và tại sao ERC777 được hoàn thiện. Tuy nhiên, nó đã gây ra đủ sự nhầm lẫn khi Openzeppelin không dùng thư viện ERC777.


Nếu bạn muốn giao thức của mình tương thích với các mã thông báo hoạt động giống như mã thông báo ERC20 nhưng có móc chuyển, thì vấn đề đơn giản là xử lý các hàm transfer và transferFrom giống như chúng sẽ thực hiện lệnh gọi hàm tới bộ nhận.


Lần vào lại ERC777 này đã xảy ra với Uniswap (Openzeppelin đã ghi lại quá trình khai thác tại đây nếu bạn tò mò).

ERC20: Không phải tất cả các mã thông báo ERC20 đều trả về true

Thông số kỹ thuật ERC20 quy định rằng mã thông báo ERC20 phải trả về true khi chuyển thành công. Bởi vì hầu hết các triển khai ERC20 không thể thất bại trừ khi khoản trợ cấp không đủ hoặc số tiền được chuyển quá nhiều, hầu hết các nhà phát triển đã quen với việc bỏ qua giá trị trả lại của mã thông báo ERC20 và cho rằng việc chuyển không thành công sẽ hoàn nguyên.


Thành thật mà nói, đây không phải là hậu quả nếu bạn chỉ làm việc với mã thông báo ERC20 đáng tin cậy mà bạn biết hành vi của nó. Nhưng khi xử lý các mã thông báo ERC20 tùy ý, sự khác biệt trong hành vi này phải được tính đến.


Có một kỳ vọng ngầm trong nhiều hợp đồng rằng các giao dịch chuyển không thành công phải luôn hoàn nguyên, không trả về giá trị sai vì hầu hết các mã thông báo ERC20 không có cơ chế trả về giá trị sai, vì vậy điều này đã dẫn đến nhiều nhầm lẫn.


Vấn đề phức tạp hơn nữa là một số mã thông báo ERC20 không tuân theo giao thức trả về giá trị thực, đặc biệt là Tether. Một số mã thông báo hoàn nguyên khi chuyển không thành công, điều này sẽ khiến việc hoàn nguyên trở nên nổi bong bóng đối với người gọi. Do đó, một số thư viện gói các lệnh gọi chuyển mã thông báo ERC20 để chặn quá trình hoàn nguyên và thay vào đó trả về một giá trị boolean.


Dưới đây là một số triển khai

Openzeppelin SafeTransfer

Solady SafeTransfer (tiết kiệm gas hơn đáng kể)

ERC20: Ngộ độc địa chỉ

Đây không phải là một lỗ hổng hợp đồng thông minh, nhưng chúng tôi đề cập đến nó ở đây cho đầy đủ.

Thông số kỹ thuật cho phép chuyển không có mã thông báo ERC20 nào. Điều này có thể dẫn đến nhầm lẫn cho các ứng dụng giao diện người dùng và có thể đánh lừa người dùng về người mà họ đã gửi mã thông báo gần đây. Metamask có nhiều hơn về điều đó trong chủ đề này.

ERC20: Hoàn toàn gồ ghề

(Theo cách nói của web3, “gồ ghề” có nghĩa là “có tấm thảm được kéo ra khỏi người bạn.”)

Không có gì ngăn cản ai đó thêm chức năng vào mã thông báo ERC20 để cho phép họ tạo, chuyển và ghi mã thông báo theo ý muốn — hoặc tự hủy hoặc nâng cấp. Vì vậy, về cơ bản, có một giới hạn về mức độ “không đáng tin cậy” của mã thông báo ERC20.


Lỗi logic trong giao thức cho vay

Khi xem xét các giao thức DeFi dựa trên hoạt động cho vay và cho vay có thể bị hỏng như thế nào, sẽ rất hữu ích khi nghĩ đến các lỗi lan truyền ở cấp độ phần mềm và ảnh hưởng đến cấp độ logic nghiệp vụ. Có rất nhiều bước để hình thành và đóng một hợp đồng trái phiếu. Dưới đây là một số vectơ tấn công để xem xét.

Những cách người cho vay thua lỗ

  • Các lỗi cho phép tiền gốc do giảm (có thể về 0) mà không thực hiện bất kỳ khoản thanh toán nào.
  • Tài sản thế chấp của người mua không thể được thanh lý khi khoản vay không được hoàn trả hoặc tài sản thế chấp giảm xuống dưới ngưỡng cho phép.
  • Nếu giao thức có cơ chế chuyển quyền sở hữu nợ, thì đây có thể là phương tiện để đánh cắp trái phiếu từ người cho vay.
  • Ngày đến hạn của khoản vay gốc hoặc các khoản thanh toán được chuyển sang một ngày sau đó một cách không hợp lệ.

Những cách mà người đi vay bị thiệt

  • Một lỗi trong đó trả lại tiền gốc không dẫn đến giảm tiền gốc.
  • Một lỗi hoặc cuộc tấn công đau buồn ngăn người dùng thực hiện thanh toán.
  • Tiền gốc hoặc lãi suất bị tăng bất hợp pháp.
  • Sự thao túng của Oracle dẫn đến phá giá tài sản thế chấp.
  • Ngày đến hạn của khoản vay gốc hoặc các khoản thanh toán được chuyển sang một ngày sớm hơn một cách không hợp lý.


Nếu tài sản thế chấp bị rút khỏi giao thức, thì cả người cho vay và người đi vay đều bị thiệt, vì người đi vay không có động cơ trả lại khoản vay và người đi vay sẽ mất tiền gốc.


Như có thể thấy ở trên, có nhiều cấp độ hơn đối với việc một giao thức DeFi bị "tấn công" hơn là một đống tiền bị rút khỏi giao thức (loại sự kiện thường được đưa tin). Đây là một lĩnh vực mà các bài tập an ninh CTF (bắt cờ) có thể gây hiểu nhầm. Mặc dù quỹ giao thức bị đánh cắp là kết quả thảm khốc nhất, nhưng nó không phải là thứ duy nhất cần chống lại.

Giá trị trả về không được kiểm tra

Có hai cách để gọi một hợp đồng thông minh bên ngoài: 1) gọi chức năng bằng định nghĩa giao diện; 2) sử dụng phương thức .call. Điều này được minh họa dưới đây

 contract A { uint256 public x; function setx(uint256 _x) external { require(_x > 10, "x must be bigger than 10"); x = _x; } } interface IA { function setx(uint256 _x) external; } contract B { function setXV1(IA a, uint256 _x) external { a.setx(_x); } function setXV2(address a, uint256 _x) external { (bool success, ) = a.call(abi.encodeWithSignature("setx(uint256)", _x)); // success is not checked! } }

Trong hợp đồng B, setXV2 có thể âm thầm thất bại nếu _x nhỏ hơn 10. Khi một hàm được gọi thông qua phương thức .call, callee có thể hoàn nguyên, nhưng cha mẹ sẽ không hoàn nguyên. Giá trị của thành công phải được kiểm tra và hành vi mã phải phân nhánh tương ứng.

Biến riêng tư

Các biến riêng tư vẫn hiển thị trên chuỗi khối, vì vậy thông tin nhạy cảm không bao giờ được lưu trữ ở đó. Nếu chúng không thể truy cập được, làm cách nào để trình xác thực có thể xử lý các giao dịch phụ thuộc vào giá trị của chúng? Các biến riêng tư không thể được đọc từ hợp đồng Solidity bên ngoài, nhưng chúng có thể được đọc ngoài chuỗi bằng ứng dụng khách Ethereum.


Để đọc một biến, bạn cần biết khe lưu trữ của nó. Trong ví dụ sau, khe lưu trữ của myPrivateVar là 0.

 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract PrivateVarExample { uint256 private myPrivateVar; constructor(uint256 _initialValue) { myPrivateVar = _initialValue; } }

Đây là mã javascript để đọc biến riêng tư của hợp đồng thông minh đã triển khai

 const Web3 = require("web3"); const PRIVATE_VAR_EXAMPLE_ADDRESS = "0x123..."; // Replace with your contract address async function readPrivateVar() { const web3 = new Web3("http://localhost:8545"); // Replace with your provider's URL // Read storage slot 0 (where 'myPrivateVar' is stored) const storageSlot = 0; const privateVarValue = await web3.eth.getStorageAt( PRIVATE_VAR_EXAMPLE_ADDRESS, storageSlot ); console.log("Value of private variable 'myPrivateVar':", web3.utils.hexToNumberString(privateVarValue)); } readPrivateVar();

Cuộc gọi đại biểu không an toàn

Cuộc gọi ủy quyền không bao giờ được sử dụng với các hợp đồng không đáng tin cậy vì nó trao toàn bộ quyền kiểm soát cho người được ủy quyền. Trong ví dụ này, hợp đồng không đáng tin cậy sẽ đánh cắp tất cả ether trong hợp đồng.

 contract UntrustedDelegateCall { constructor() payable { require(msg.value == 1 ether); } function doDelegateCall(address _delegate, bytes calldata data) public { (bool ok, ) = _delegate.delegatecall(data); require(ok, "delegatecall failed"); } } contract StealEther { function steal() public { // you could also selfdestruct here // if you really wanted to be mean (bool ok,) = tx.origin.call{value: address(this).balance}(""); require(ok); } function attack(address victim) public { UntrustedDelegateCall(victim).doDelegateCall( address(this), abi.encodeWithSignature("steal()")); } }

Nâng cấp các lỗi liên quan đến proxy

Chúng tôi không thể làm công lý cho chủ đề này trong một phần duy nhất. Hầu hết các lỗi nâng cấp nói chung có thể tránh được bằng cách sử dụng plugin hardhat từ Openzeppelin và đọc về những vấn đề mà nó bảo vệ chống lại. ( https://docs.openzeppelin.com/upgrades-plugins/1.x/ ).


Tóm lại, đây là các vấn đề liên quan đến nâng cấp hợp đồng thông minh:

  • tự hủy và ủy quyền không nên được sử dụng trong các hợp đồng triển khai
  • phải cẩn thận để các biến lưu trữ không bao giờ ghi đè lên nhau trong quá trình nâng cấp
  • Nên tránh gọi các thư viện bên ngoài trong các hợp đồng triển khai vì không thể dự đoán chúng sẽ ảnh hưởng đến quyền truy cập bộ nhớ như thế nào
  • người triển khai không bao giờ được bỏ qua việc gọi hàm khởi tạo
  • không bao gồm biến khoảng cách trong hợp đồng cơ sở để tránh xung đột lưu trữ khi các biến mới được thêm vào hợp đồng cơ sở (điều này được tự động xử lý bởi plugin hardhat)
  • các giá trị trong các biến bất biến không được bảo toàn giữa các lần nâng cấp
  • làm bất cứ điều gì trong hàm tạo rất không được khuyến khích vì các bản nâng cấp trong tương lai sẽ phải thực hiện logic hàm tạo giống hệt nhau để duy trì khả năng tương thích.

Quản trị viên áp đảo

Chỉ vì hợp đồng có chủ sở hữu hoặc quản trị viên, điều đó không có nghĩa là quyền lực của họ cần phải vô hạn. Hãy xem xét một NFT. Việc chỉ chủ sở hữu rút tiền thu nhập từ việc bán NFT là hợp lý, nhưng việc có thể tạm dừng hợp đồng (chuyển khối) có thể tàn phá nếu khóa riêng tư của chủ sở hữu bị xâm phạm. Nói chung, các đặc quyền của quản trị viên phải ở mức tối thiểu nhất có thể để giảm thiểu rủi ro không cần thiết.


Nói về quyền sở hữu hợp đồng…

Sử dụng Ownable2Step thay vì Ownable

Về mặt kỹ thuật, đây không phải là một lỗ hổng, nhưng OpenZeppelin có thể sở hữu có thể dẫn đến mất quyền sở hữu hợp đồng nếu quyền sở hữu được chuyển đến một địa chỉ không tồn tại. Ownable2step yêu cầu người nhận xác nhận quyền sở hữu. Điều này đảm bảo không vô tình gửi quyền sở hữu đến một địa chỉ bị nhập sai.

Lỗi làm tròn

Solidity không có float nên lỗi làm tròn là không thể tránh khỏi. Nhà thiết kế phải ý thức được điều đúng đắn cần làm là làm tròn lên hay làm tròn xuống, và việc làm tròn nên có lợi cho ai.


Phép chia phải luôn được thực hiện sau cùng. Đoạn mã sau chuyển đổi không chính xác giữa các stablecoin có số thập phân khác nhau. Cơ chế trao đổi sau đây cho phép người dùng nhận miễn phí một lượng nhỏ USDC (có 6 chữ số thập phân) khi đổi lấy dai (có 18 chữ số thập phân). Biến daiToTake sẽ làm tròn xuống 0, không lấy gì từ người dùng để đổi lấy một usdcAmount khác không.

 contract Exchange { uint256 private constant CONVERSION = 1e12; function swapDAIForUSDC(uint256 usdcAmount) external pure returns (uint256 a) { uint256 daiToTake = usdcAmount / CONVERSION; conductSwap(daiToTake, usdcAmount); } }

Chạy phía trước

Chạy trước trong bối cảnh của Etheruem (và các chuỗi tương tự) có nghĩa là quan sát một giao dịch đang chờ xử lý và thực hiện một giao dịch khác trước giao dịch đó bằng cách trả giá gas cao hơn. Tức là kẻ tấn công đã “chạy trước” giao dịch. Nếu giao dịch là một giao dịch có lợi nhuận, thì bạn nên sao chép chính xác giao dịch ngoại trừ việc trả giá xăng cao hơn. Hiện tượng này đôi khi được gọi là MEV, có nghĩa là giá trị có thể trích xuất của người khai thác, nhưng đôi khi giá trị có thể trích xuất tối đa trong các ngữ cảnh khác. Các nhà sản xuất khối có quyền lực vô hạn để sắp xếp lại các giao dịch và chèn giao dịch của riêng họ, và trong lịch sử, các nhà sản xuất khối là những người khai thác trước khi Ethereum chuyển sang bằng chứng cổ phần, do đó có tên này.

Frontrunning: Rút tiền không được bảo vệ

Rút Ether từ một hợp đồng thông minh có thể được coi là một “giao dịch có lãi”. Bạn thực hiện một giao dịch không mất phí (ngoại trừ tiền xăng) và kết thúc với nhiều tiền điện tử hơn so với ban đầu.

 contract UnprotectedWithdraw { constructor() payable { require(msg.value == 1 ether, "must create with 1 eth"); } function unsafeWithdraw() external { (bool ok, ) = msg.sender.call{value: address(this).value}(""); require(ok, "transfer failed"). } }


Nếu bạn triển khai hợp đồng này và cố gắng rút tiền, một bot chạy trước sẽ nhận thấy lệnh gọi “unsafeWithdraw” của bạn trong mempool và sao chép nó để nhận Ether trước.

Chạy trước: Tấn công lạm phát ERC4626, sự kết hợp của lỗi chạy trước và làm tròn

Chúng tôi đã viết sâu về cuộc tấn công lạm phát ERC-4626 trong hướng dẫn ERC4626 của chúng tôi. Nhưng ý chính của nó là hợp đồng ERC4626 phân phối mã thông báo “chia sẻ” dựa trên tỷ lệ phần trăm “tài sản” mà một nhà giao dịch đóng góp.


Đại khái, nó hoạt động như sau:

 function getShares(...) external { // code shares_received = assets_contributed / total_assets; // more code }

Tất nhiên, sẽ không ai đóng góp tài sản và không nhận lại cổ phần, nhưng họ không thể dự đoán điều đó sẽ xảy ra nếu ai đó có thể thực hiện giao dịch trước để lấy cổ phần.


Ví dụ: họ đóng góp 200 tài sản khi nhóm có 20 tài sản, họ mong muốn nhận được 100 cổ phần. Nhưng nếu ai đó thực hiện trước giao dịch để ký gửi 200 tài sản, thì công thức sẽ là 200/220, làm tròn xuống 0, khiến nạn nhân mất tài sản và không nhận lại được cổ phiếu nào.

Chạy trước: Phê duyệt ERC20

Tốt nhất là minh họa điều này bằng một ví dụ thực tế hơn là mô tả nó trong phần tóm tắt


  1. Giả sử Alice chấp thuận cho Eve 100 mã thông báo. (Eve luôn là kẻ ác, không phải Bob, vì vậy chúng tôi sẽ tuân theo quy ước).
  2. Alice đổi ý và gửi một giao dịch để thay đổi sự chấp thuận của Eve thành 50.
  3. Trước khi giao dịch thay đổi phê duyệt thành 50 được bao gồm trong khối, nó nằm trong mempool nơi Eve có thể nhìn thấy nó.
  4. Eve gửi một giao dịch để yêu cầu 100 mã thông báo của cô ấy để chạy trước sự chấp thuận cho 50.
  5. Sự chấp thuận cho 50 đi qua
  6. Eve thu thập 50 mã thông báo.


Giờ đây, Eve có 150 mã thông báo thay vì 100 hoặc 50. Giải pháp cho vấn đề này là đặt phê duyệt thành 0 trước khi tăng hoặc giảm mã khi xử lý các phê duyệt không đáng tin cậy.

Frontrunning: Tấn công bánh sandwich

Giá của một tài sản di chuyển để đáp ứng với áp lực mua và bán. Nếu một đơn đặt hàng lớn đang nằm trong mempool, các nhà giao dịch có động cơ sao chép đơn đặt hàng nhưng với giá gas cao hơn. Bằng cách đó, họ mua tài sản, để lệnh lớn đẩy giá lên, sau đó họ bán ngay. Lệnh bán đôi khi được gọi là "chạy lùi". Lệnh bán có thể được thực hiện bằng cách đặt lệnh bán với giá gas thấp hơn để trình tự trông như thế này


  1. mua trước
  2. mua lớn
  3. bán


Biện pháp phòng thủ chính chống lại cuộc tấn công này là cung cấp thông số “trượt giá”. Nếu bản thân lệnh “mua trước” đẩy giá lên vượt quá một ngưỡng nhất định, thì lệnh “mua lớn” sẽ hoàn nguyên khiến cho lệnh mua trước thất bại trong giao dịch.


Nó được gọi là giao dịch mua lớn, bởi vì giao dịch mua lớn được bao gồm bởi giao dịch mua trước và bán sau. Cuộc tấn công này cũng hoạt động với các lệnh bán lớn, chỉ theo hướng ngược lại.

Tìm hiểu thêm về chạy trước

Frontrunning là một chủ đề lớn. Flashbots đã nghiên cứu rộng rãi về chủ đề này và xuất bản một số công cụ cũng như bài viết nghiên cứu để giúp giảm thiểu các tác động tiêu cực từ bên ngoài.


Liệu frontrunning có thể được “thiết kế riêng” với kiến trúc blockchain phù hợp hay không vẫn là một chủ đề tranh luận chưa được giải quyết dứt điểm. Hai bài báo sau đây là những tác phẩm kinh điển lâu dài về chủ đề này:


Ethereum là một khu rừng tối

Thoát khỏi khu rừng tối

Chữ Ký Liên Quan

Chữ ký số có hai cách sử dụng trong bối cảnh hợp đồng thông minh:

  • cho phép các địa chỉ ủy quyền một số giao dịch trên chuỗi khối mà không cần thực hiện giao dịch thực tế
  • chứng minh với một hợp đồng thông minh rằng người gửi có một số quyền để làm điều gì đó, theo một địa chỉ được xác định trước

Dưới đây là một ví dụ về việc sử dụng chữ ký số một cách an toàn để cung cấp cho người dùng đặc quyền đúc NFT:

 import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract NFT is ERC721("name", "symbol") { function mint(bytes calldata signature) external { address recovered = keccak256(abi.encode(msg.sender)).toEthSignedMessageHash().recover(signature); require(recovered == authorizer, "signature does not match"); } }

Một ví dụ kinh điển là chức năng Phê duyệt trong ERC20. Để phê duyệt một địa chỉ để rút một số lượng mã thông báo nhất định từ tài khoản của chúng tôi, chúng tôi phải thực hiện một giao dịch Ethereum thực tế, tốn phí gas.


Đôi khi, việc chuyển chữ ký số cho người nhận ngoài chuỗi sẽ hiệu quả hơn, sau đó người nhận cung cấp chữ ký cho hợp đồng thông minh để chứng minh rằng họ được ủy quyền thực hiện giao dịch.


ERC20Permit cho phép phê duyệt bằng chữ ký điện tử. Chức năng được mô tả như sau

 function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public

Thay vì gửi một giao dịch phê duyệt thực tế, chủ sở hữu có thể “ký” phê duyệt cho người chi tiêu (cùng với thời hạn). Sau đó, người chi tiêu được phê duyệt có thể gọi chức năng cấp phép với các tham số được cung cấp.

Giải phẫu của một chữ ký

Bạn sẽ thấy các biến v, r và s thường xuyên. Chúng được thể hiện một cách vững chắc với các kiểu dữ liệu uint8, bytes32 và bytes32 tương ứng. Đôi khi, các chữ ký được biểu diễn dưới dạng một mảng 65 byte, tất cả các giá trị này được nối với nhau dưới dạng abi.encodePacked(r, s, v);


Hai thành phần thiết yếu khác của chữ ký là hàm băm thông báo (32 byte) và địa chỉ ký. Trình tự trông như thế này


  1. Khóa riêng (privKey) được sử dụng để tạo địa chỉ công khai (ethAddress)

  2. Một hợp đồng thông minh lưu trữ ethAddress trước

  3. Người dùng ngoại tuyến băm một tin nhắn và ký mã băm. Điều này tạo ra cặp msgHash và chữ ký (r, s, v)

  4. Hợp đồng thông minh nhận một tin nhắn, băm nó để tạo msgHash, sau đó kết hợp nó với (r, s, v) để xem địa chỉ nào xuất hiện.

  5. Nếu địa chỉ khớp với ethAddress, thì chữ ký hợp lệ (theo một số giả định nhất định mà chúng ta sẽ sớm thấy!)


Hợp đồng thông minh sử dụng ecrecover hợp đồng được biên dịch sẵn ở bước 4 để thực hiện cái mà chúng tôi gọi là kết hợp và lấy lại địa chỉ.


Có rất nhiều bước trong quá trình này mà mọi thứ có thể đi ngang.

Chữ ký: ecrecover trả về địa chỉ (0) và không hoàn nguyên khi địa chỉ không hợp lệ

Điều này có thể dẫn đến lỗ hổng nếu một biến chưa được khởi tạo được so sánh với đầu ra của ecrecover.

Mã này dễ bị tấn công

 contract InsecureContract { address signer; // defaults to address(0) // who lets us give the beneficiary the airdrop without them// spending gas function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { // ecrecover returns address(0) if the signature is invalid require(signer == ecrecover(keccak256(abi.encode(who, amount)), v, r, s), "invalid signature"); mint(msg.sender, AIRDROP_AMOUNT); } }

phát lại chữ ký

Phát lại chữ ký xảy ra khi hợp đồng không theo dõi nếu chữ ký đã được sử dụng trước đó. Trong đoạn mã sau, chúng tôi khắc phục sự cố trước đó, nhưng sự cố vẫn không an toàn.

 contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); mint(msg.sender, amount); } }

Mọi người có thể yêu cầu airdrop bao nhiêu lần tùy thích!


Chúng ta có thể thêm các dòng sau

 bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // mapping(bytes => bool); used[signature] = true;

Than ôi, mã vẫn không an toàn!

chữ ký dễ uốn

Đưa ra một chữ ký hợp lệ, kẻ tấn công có thể thực hiện một số phép tính nhanh để lấy được một chữ ký khác. Sau đó, kẻ tấn công có thể “phát lại” chữ ký đã sửa đổi này. Nhưng trước tiên, hãy cung cấp một số mã chứng minh rằng chúng ta có thể bắt đầu với một chữ ký hợp lệ, sửa đổi nó và hiển thị chữ ký mới vẫn được thông qua.

 contract Malleable { // v = 28 // r = 0xf8479d94c011613baeffe9239e4ff65e2adbac744c34217ca7d51378e72c5204 // s = 0x57af17590a914b759c45aaeabaf513d5ef72d7da1bdd19d9f2e1bc371ece5b86 // m = 0x0000000000000000000000000000000000000000000000000000000000000003 function foo(bytes calldata msg, uint8 v, bytes32 r, bytes32 s) public pure returns (address, address){ bytes32 h = keccak256(msg); address a = ecrecover(h, v, r, s); // The following is math magic to invert the // signature and create a valid one // flip s bytes32 s2 = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint256(s)); // invert v uint8 v2; require(v == 27 || v == 28, "invalid v"); v2 = v == 27 ? 28 : 27; address b = ecrecover(h, v2, r, s2); assert(a == b); // different signatures, same address!; return (a, b); } }

Như vậy, ví dụ đang chạy của chúng tôi vẫn dễ bị tổn thương. Sau khi ai đó đưa ra chữ ký hợp lệ, chữ ký hình ảnh phản chiếu đó có thể được tạo ra và bỏ qua việc kiểm tra chữ ký đã sử dụng.

 contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // this can be bypassed used[signature] = true; mint(msg.sender, amount); } }

Chữ ký an toàn

Bạn có thể muốn một số mã chữ ký an toàn vào thời điểm này, phải không? Chúng tôi giới thiệu bạn đến hướng dẫn của chúng tôi về cách tạo chữ ký ở dạng rắn và thử nghiệm chúng trong xưởng đúc.


Nhưng đây là danh sách kiểm tra.


  • Sử dụng thư viện của openzeppelin để ngăn chặn các cuộc tấn công dễ uốn nắn và khôi phục các sự cố bằng không
  • Không sử dụng chữ ký làm mật khẩu. Các tin nhắn cần chứa thông tin mà kẻ tấn công không thể dễ dàng sử dụng lại (ví dụ: msg.sender)
  • Băm những gì bạn đang ký trên chuỗi
  • Sử dụng một nonce để ngăn chặn các cuộc tấn công phát lại. Tốt hơn nữa, hãy theo dõi EIP712 để những người sử dụng có thể thấy những gì họ đang ký và bạn có thể ngăn chữ ký được sử dụng lại giữa các hợp đồng và các chuỗi khác nhau.

Chữ ký có thể được giả mạo hoặc thủ công mà không có biện pháp bảo vệ thích hợp

Cuộc tấn công ở trên có thể được tổng quát hóa hơn nữa nếu việc băm không được thực hiện trên chuỗi. Trong các ví dụ trên, việc băm được thực hiện trong hợp đồng thông minh, vì vậy các ví dụ trên không dễ bị khai thác sau đây.


Hãy xem mã để khôi phục chữ ký

 // this code is vulnerable! function recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public returns (address signer) { require(signer == ecrecover(hash, v, r, s), "signer does not match"); // more actions }


Người dùng cung cấp cả hàm băm và chữ ký. Nếu kẻ tấn công đã nhìn thấy chữ ký hợp lệ từ người ký, họ có thể chỉ cần sử dụng lại hàm băm và chữ ký của một thư khác.

Đây là lý do tại sao việc băm thông điệp trong hợp đồng thông minh , không phải ngoài chuỗi là rất quan trọng.

Để xem hoạt động khai thác này, hãy xem CTF mà chúng tôi đã đăng trên Twitter.


Thử thách ban đầu:

Phần 1: https://twitter.com/RareSkills_io/status/1650869999266037760

Phần 2: https://twitter.com/RareSkills_io/status/1650897671543197701

Các giải pháp:

https://twitter.com/RareSkills_io/status/1651527648676573185 https://twitter.com/RareSkills_io/status/1651224817465540611

Chữ ký như định danh

Chữ ký không nên được sử dụng để xác định người dùng. Do tính linh hoạt, chúng không thể được coi là duy nhất. Msg.sender có đảm bảo tính duy nhất mạnh mẽ hơn nhiều.

Một số phiên bản trình biên dịch Solidity có lỗi

Xem một bài tập bảo mật mà chúng tôi đã tổ chức trên Twitter tại đây . Khi kiểm tra cơ sở mã, hãy kiểm tra phiên bản Solidity với các thông báo phát hành trên trang Solidity để xem liệu có thể có lỗi hay không.

Giả sử hợp đồng thông minh là bất biến

Hợp đồng thông minh có thể được nâng cấp bằng Mẫu ủy quyền (hoặc hiếm hơn là mẫu biến chất). Hợp đồng thông minh không nên dựa vào chức năng của một hợp đồng thông minh tùy ý để không thay đổi.

Chuyển () và gửi () có thể bị hỏng với ví đa chữ ký

Không nên sử dụng các chức năng solidity chuyển và gửi. Họ cố tình giới hạn lượng gas được chuyển tiếp với giao dịch ở mức 2.300, điều này sẽ khiến hầu hết các hoạt động hết gas.


Ví đa chữ ký an toàn gnosis thường được sử dụng hỗ trợ chuyển tiếp cuộc gọi đến một địa chỉ khác trong chức năng dự phòng . Nếu ai đó sử dụng chuyển hoặc gửi để gửi Ether đến ví đa chữ ký, chức năng dự phòng có thể hết gas và quá trình chuyển sẽ không thành công. Ảnh chụp màn hình chức năng dự phòng an toàn của gnosis được cung cấp bên dưới. Bạn đọc có thể thấy rõ là có quá đủ thao tác để sử dụng hết 2300 gas.


Nếu bạn cần tương tác với một hợp đồng sử dụng chuyển và gửi, hãy xem bài viết của chúng tôi về các giao dịch danh sách truy cập Ethereum cho phép bạn giảm chi phí gas cho các hoạt động truy cập hợp đồng và lưu trữ.

Là tràn số học vẫn có liên quan?

Solidity 0.8.0 được tích hợp tính năng chống tràn và chống tràn. Vì vậy, trừ khi có một khối không được kiểm tra hoặc mã cấp thấp trong Yul được sử dụng, không có nguy cơ tràn. Do đó, không nên sử dụng các thư viện SafeMath vì chúng gây lãng phí khí cho các lần kiểm tra bổ sung.

Thế còn block.timestamp thì sao?

Một số tài liệu cho rằng block.timestamp là một vectơ dễ bị tổn thương vì những người khai thác có thể thao túng nó. Điều này thường áp dụng cho việc sử dụng dấu thời gian như một nguồn ngẫu nhiên, điều này không nên được thực hiện như tài liệu trước đó. Ethereum sau hợp nhất cập nhật dấu thời gian trong khoảng thời gian chính xác 12 giây (hoặc bội số của 12 giây). Tuy nhiên, đo thời gian ở mức độ chi tiết cấp hai là một phản mẫu. Trong phạm vi một phút, sẽ có khả năng xảy ra lỗi đáng kể nếu trình xác thực bỏ lỡ vị trí khối của họ và khoảng cách 24 giây trong quá trình tạo khối xảy ra.

Các trường hợp góc, trường hợp cạnh và tắt bởi một lỗi

Các trường hợp góc không thể dễ dàng xác định, nhưng một khi bạn đã thấy đủ về chúng, bạn bắt đầu phát triển trực giác về chúng. Một trường hợp góc có thể giống như một người nào đó đang cố gắng nhận phần thưởng, nhưng không có gì đặt cược. Điều này hợp lệ, chúng ta chỉ nên trao cho họ phần thưởng bằng không. Tương tự như vậy, chúng ta thường muốn chia đều phần thưởng, nhưng nếu chỉ có một người nhận và về mặt kỹ thuật thì không có sự phân chia nào xảy ra?

Trường hợp góc: Ví dụ 1

Ví dụ này được lấy từ chuỗi twitter của Akshay Srivastav và đã được sửa đổi.

Xem xét trường hợp ai đó có thể thực hiện một hành động đặc quyền nếu một tập hợp các địa chỉ đặc quyền cung cấp chữ ký cho hành động đó.

 contract VulnerableMultisigAuthorization { struct Authorization { bytes signature; address authorizer; bytes32 hashOfAction; // more fields } // more codef unction takeAction(Authorization[] calldata auths, bytes calldata action) public { // logic for avoiding replay attacks for (uint256 i; i < auths.length; ++i) { require(validateSignature(auths[i].signature, auths[i].authorizer), "invalid signature"); require(authorizers[auths[i].authorizer], "address is not an authorizer"); } doTheAction(action) } }

Nếu bất kỳ chữ ký nào không hợp lệ hoặc chữ ký không khớp với địa chỉ hợp lệ, việc hoàn nguyên sẽ xảy ra. Nhưng nếu mảng trống thì sao? Trong trường hợp đó, nó sẽ chuyển xuống doTheAction mà không cần bất kỳ chữ ký nào.

Tắt từng cái một: Ví dụ 2

 contract ProportionalRewards { mapping(address => uint256) originalId; address[] stakers; function stake(uint256 id) public { nft.transferFrom(msg.sender, address(this), id); stakers.append(msg.sender); } function unstake(uint256 id) public { require(originalId[id] == msg.sender, "not the owner"); removeFromArray(msg.sender, stakers); sendRewards(msg.sender, totalRewardsSinceLastclaim() / stakers.length()); nft.transferFrom(address(this), msg.sender, id); } }

Mặc dù đoạn mã trên không hiển thị tất cả các triển khai chức năng, ngay cả khi các chức năng hoạt động như tên mô tả của chúng, thì vẫn có lỗi. Bạn có thể phát hiện ra nó? Đây là một hình ảnh để cung cấp cho bạn một số khoảng trống để không nhìn thấy câu trả lời trước khi bạn cuộn xuống.

Hàm removeFromArray và sendRewards sai thứ tự. Nếu chỉ có một người dùng trong mảng stakers, sẽ có lỗi chia cho 0 và người dùng sẽ không thể rút NFT của họ. Hơn nữa, phần thưởng có thể không được chia theo cách mà tác giả dự định. Nếu có bốn người đặt cược ban đầu và một người rút tiền, anh ta sẽ nhận được một phần ba phần thưởng vì độ dài mảng là 3 tại thời điểm rút tiền.

Trường hợp góc Ví dụ 3: Tính toán sai phần thưởng tài chính gộp

Hãy sử dụng một ví dụ thực tế mà theo một số ước tính đã gây ra thiệt hại hơn 100 triệu đô la. Đừng lo lắng nếu bạn không hiểu đầy đủ về giao thức Hợp chất, chúng tôi sẽ chỉ tập trung vào các phần có liên quan. (Ngoài ra, giao thức Compound là một trong những giao thức quan trọng và mang tính hệ quả nhất trong lịch sử của DeFi, chúng tôi dạy nó trong chương trình đào tạo DeFi của chúng tôi, vì vậy nếu đây là ấn tượng đầu tiên của bạn về giao thức, thì đừng hiểu lầm).


Dù sao, mục đích của Compound là thưởng cho người dùng vì đã cho các nhà giao dịch khác vay tiền điện tử nhàn rỗi của họ, những người có thể sử dụng nó. Người cho vay được trả cả tiền lãi và bằng mã thông báo COMP (người vay có thể yêu cầu phần thưởng mã thông báo COMP, nhưng chúng tôi sẽ không tập trung vào điều đó ngay bây giờ).

Bộ điều khiển hợp chất là một hợp đồng ủy quyền ủy quyền các cuộc gọi đến các triển khai có thể được thiết lập bởi Quản trị hợp chất.


Theo đề xuất quản trị 62 vào ngày 30 tháng 9 năm 2021, hợp đồng triển khai được đặt thành hợp đồng triển khai có lỗ hổng. Cùng ngày nó được phát hành trực tuyến, người ta đã quan sát thấy trên Twitter rằng một số giao dịch đang yêu cầu phần thưởng COMP mặc dù không đặt cược mã thông báo.

Hàm dễ bị tấn côngphân phốiSupplierComp()


Đây là mã gốc


Trớ trêu thay, lỗi lại nằm trong nhận xét TODO. “Không phân phối COMP của nhà cung cấp nếu người dùng không ở trong thị trường của nhà cung cấp.” Nhưng không có kiểm tra trong mã cho điều đó. Miễn là người dùng giữ mã thông báo đặt cược trong ví của họ (CToken(cToken).balanceOf(nhà cung cấp);), thì

Đề xuất 64 đã sửa lỗi vào ngày 9 tháng 10 năm 2021.


Mặc dù đây có thể được coi là một lỗi xác thực đầu vào, nhưng người dùng đã không gửi bất kỳ thứ gì độc hại. Nếu ai đó cố gắng yêu cầu phần thưởng vì không đặt cược bất cứ thứ gì, tính toán chính xác sẽ bằng không. Có thể cho rằng, đó là lỗi logic nghiệp vụ hoặc trường hợp góc.

Hack trong thế giới thực

Các vụ hack DeFi xảy ra trong thế giới thực thường không thuộc các loại tốt ở trên.

Đóng băng ví Pairity (tháng 11 năm 2017)

Ví chẵn lẻ không nhằm mục đích sử dụng trực tiếp. Đó là một triển khai tham chiếu mà các bản sao hợp đồng thông minh sẽ chỉ ra. Việc triển khai cho phép các bản sao tự hủy nếu muốn, nhưng điều này yêu cầu tất cả chủ sở hữu ví phải đăng nhập vào nó.

 // throw unless the contract is not yet initialized.modifier only_uninitialized { if (m_numOwners > 0) throw; _; } function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized { initDaylimit(_daylimit); initMultiowned(_owners, _required); }

Chủ sở hữu ví được khai báo

 // kills the contract sending everything to `_to`.function kill(address _to) onlymanyowners(sha3(msg.data)) external { suicide(_to); }

Một số tài liệu mô tả đây là “sự tự hủy không được bảo vệ” tức là lỗi kiểm soát truy cập, nhưng điều này không hoàn toàn chính xác. Vấn đề là hàm initWallet không được gọi trong hợp đồng triển khai và điều đó cho phép ai đó tự gọi hàm initWallet và biến họ thành chủ sở hữu. Điều đó đã cho họ quyền gọi hàm kill. Nguyên nhân gốc rễ là do việc triển khai không được khởi tạo. Do đó, lỗi được đưa ra không phải do mã solidity bị lỗi mà do quy trình triển khai bị lỗi.

Hack DAO của Badger (tháng 12 năm 2021)

Không có mã Solidity nào bị khai thác trong vụ hack này. Thay vào đó, những kẻ tấn công lấy khóa API Cloudflare và đưa một tập lệnh vào giao diện người dùng của trang web làm thay đổi giao dịch của người dùng để rút tiền trực tiếp đến địa chỉ của kẻ tấn công. Đọc thêm trong bài viết này.

Vectơ tấn công cho ví

Khóa riêng không đủ ngẫu nhiên

Động lực để khám phá các địa chỉ có nhiều số 0 đứng đầu là chúng sử dụng gas hiệu quả hơn. Một giao dịch Ethereum được tính 4 gas cho một byte bằng 0 trong dữ liệu giao dịch và 16 gas cho một byte khác không.


Như vậy, Wintermute đã bị tấn công vì sử dụng địa chỉ tục tĩu ( writeup ). Đây là bài viết của 1inch về cách trình tạo địa chỉ tục tĩu bị xâm phạm.


Ví ủy thác có một lỗ hổng tương tự được ghi lại trong bài viết này ( https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been- bị đánh cắp / )


Lưu ý rằng điều này không áp dụng cho các hợp đồng thông minh có số 0 đứng đầu được phát hiện bằng cách thay đổi muối trong create2, vì hợp đồng thông minh không có khóa riêng.

Các nonce được sử dụng lại hoặc các nonce không đủ ngẫu nhiên.

Điểm “r” và “s” trên chữ ký Đường cong Elliptic được tạo như sau

 r = k * G (mod N) s = k^-1 * (h + r * privateKey) (mod N)


G, r, s, h, an N đều được biết đến một cách công khai. Nếu “k” trở thành công khai, thì “privateKey” là biến không xác định duy nhất và có thể giải quyết được. Vì điều này, các ví cần tạo k hoàn toàn ngẫu nhiên và không bao giờ sử dụng lại nó. Nếu tính ngẫu nhiên không hoàn toàn ngẫu nhiên, thì có thể suy ra k.


Việc tạo ngẫu nhiên không an toàn trong thư viện Java đã khiến rất nhiều ví bitcoin của Android dễ bị tấn công vào năm 2013. (Bitcoin sử dụng thuật toán chữ ký giống như Ethereum.)


( https://arstechnica.com/information-t Technology/2013/ 08/all-android-created-bitcoin-wallets-vulnerable-to-theft/ ).

Hầu hết các lỗ hổng là ứng dụng cụ thể

Rèn luyện bản thân để nhanh chóng nhận ra các phản mẫu trong danh sách này sẽ giúp bạn trở thành một lập trình viên hợp đồng thông minh hiệu quả hơn, nhưng hậu quả của hầu hết các lỗi hợp đồng thông minh là do sự không phù hợp giữa logic nghiệp vụ dự định và những gì mã thực sự làm.


Các khu vực khác có thể xảy ra lỗi:

  • khuyến khích tokenomic xấu
  • tắt bởi một lỗi
  • lỗi đánh máy
  • quản trị viên hoặc người dùng bị đánh cắp khóa riêng

Nhiều lỗ hổng có thể đã bị phát hiện với các bài kiểm tra đơn vị

Thử nghiệm đơn vị hợp đồng thông minh được cho là biện pháp bảo vệ cơ bản nhất cho hợp đồng thông minh, nhưng một số lượng đáng kinh ngạc các hợp đồng thông minh thiếu chúng hoặc không có đủ phạm vi kiểm tra .

Nhưng các bài kiểm tra đơn vị có xu hướng chỉ kiểm tra “con đường hạnh phúc” (hành vi dự kiến/được thiết kế) của các hợp đồng. Để kiểm tra các trường hợp đáng ngạc nhiên, các phương pháp kiểm tra bổ sung phải được áp dụng.


Trước khi một hợp đồng thông minh được gửi để kiểm tra, trước tiên cần thực hiện những việc sau:

  • Phân tích tĩnh với các công cụ như Slither để đảm bảo không bỏ sót các lỗi cơ bản
  • Bảo hiểm 100% dòng và nhánh thông qua thử nghiệm đơn vị
  • Thử nghiệm đột biến để đảm bảo các bài kiểm tra đơn vị có các câu khẳng định mạnh mẽ
  • Kiểm tra Fuzz, đặc biệt là đối với số học
  • Thử nghiệm bất biến cho các thuộc tính trạng thái
  • Xác minh chính thức khi thích hợp


Đối với những người không quen thuộc với một số phương pháp ở đây, Patrick Collins của Cyfrin Audits có phần giới thiệu hài hước về làm mờ trạng thái và không trạng thái trong video của anh ấy .


Các công cụ để hoàn thành các nhiệm vụ này đang nhanh chóng trở nên phổ biến và dễ sử dụng hơn.

Nhiêu tai nguyên hơn

Một số tác giả đã tổng hợp danh sách các vụ hack DeFi trước đây trong các Repos này:


Secureum đã được sử dụng rộng rãi để nghiên cứu và thực hành bảo mật, nhưng hãy nhớ rằng repo đã không được cập nhật đáng kể trong 2 năm


Bạn có thể thực hành khai thác các lỗ hổng solidity với kho lưu trữ Solidity Riddles của chúng tôi.


DamVulnerableDeFi là một trò chơi chiến tranh cổ điển mà mọi nhà phát triển nên luyện tập


Capture The Ether và Ethernaut là kinh điển, nhưng hãy nhớ rằng một số vấn đề dễ đến mức phi thực tế hoặc dạy các khái niệm Solidity đã lỗi thời


Một số công ty bảo mật cộng đồng có uy tín có một danh sách hữu ích về các cuộc kiểm toán trước đây để nghiên cứu.

Trở thành kiểm toán viên hợp đồng thông minh

Nếu bạn không thông thạo Solidity, thì không có cách nào bạn có thể kiểm tra các hợp đồng thông minh Ethereum.


Không có chứng chỉ được công nhận trong ngành để trở thành kiểm toán viên hợp đồng thông minh. Bất kỳ ai cũng có thể tạo một trang web và hồ sơ trên mạng xã hội tự xưng là kiểm toán viên vững chắc và bắt đầu bán dịch vụ, và nhiều người đã làm như vậy. Do đó, hãy thận trọng và nhận được sự giới thiệu trước khi thuê một người.

Để trở thành kiểm toán viên hợp đồng thông minh, bạn cần giỏi hơn đáng kể so với nhà phát triển solidity trung bình trong việc phát hiện lỗi. Như vậy, “lộ trình” để trở thành một kiểm toán viên không gì khác hơn là hàng tháng trời luyện tập không ngừng và có chủ ý cho đến khi bạn trở thành người bắt lỗi hợp đồng thông minh tốt hơn hầu hết.


Nếu bạn thiếu quyết tâm vượt trội so với các đồng nghiệp của mình trong việc xác định các lỗ hổng, thì không có khả năng bạn sẽ phát hiện ra các vấn đề quan trọng trước khi những tên tội phạm có động cơ và được đào tạo bài bản phát hiện.

Sự thật phũ phàng về cơ hội thành công của bạn khi trở thành kiểm toán viên bảo mật hợp đồng thông minh

Kiểm toán hợp đồng thông minh gần đây đã được coi là một lĩnh vực hấp dẫn để làm việc do nhận thức rằng nó sinh lợi. Thật vậy, một số khoản thanh toán tiền thưởng cho lỗi đã vượt quá 1 triệu đô la, nhưng đây là trường hợp ngoại lệ cực kỳ hiếm, không phải là tiêu chuẩn.


Code4rena có một bảng xếp hạng công khai về các khoản thanh toán từ các đối thủ cạnh tranh trong các cuộc thi kiểm toán của họ, cung cấp cho chúng tôi một số dữ liệu về tỷ lệ thành công.


Có 1171 tên trên bảng, chưa

  • Chỉ có 29 đối thủ cạnh tranh có thu nhập trọn đời trên 100.000 đô la (2,4%)
  • Chỉ 57 người có thu nhập cả đời trên 50.000 USD (4,9%)
  • Chỉ 170 người có thu nhập cả đời trên 10.000 đô la (14,5%)


Ngoài ra, hãy xem xét điều này, khi Openzeppelin mở đơn xin học bổng nghiên cứu bảo mật (không phải công việc, sàng lọc và đào tạo trước khi làm việc), họ đã nhận được hơn 300 đơn đăng ký chỉ để chọn ít hơn 10 ứng viên, trong số đó thậm chí còn ít hơn nữa. công việc thời gian.

Đơn xin việc kiểm toán viên hợp đồng thông minh OpenZeppelin

Đó là tỷ lệ nhập học thấp hơn Harvard.


Kiểm toán hợp đồng thông minh là một trò chơi có tổng bằng không. Chỉ có rất nhiều dự án để kiểm tra, chỉ có rất nhiều ngân sách cho bảo mật và chỉ có rất nhiều lỗi để tìm. Nếu bạn bắt đầu nghiên cứu về bảo mật ngay bây giờ, sẽ có hàng chục cá nhân và nhóm có động lực cao hỗ trợ bạn rất nhiều. Hầu hết các dự án sẵn sàng trả phí cho một kiểm toán viên có uy tín hơn là một kiểm toán viên mới chưa được kiểm tra.


Trong bài viết này, chúng tôi đã liệt kê ít nhất 20 loại lỗ hổng bảo mật khác nhau. Nếu bạn dành một tuần để thành thạo từng thứ (điều này hơi lạc quan), thì bạn chỉ mới bắt đầu hiểu kiến thức phổ biến đối với các kiểm toán viên có kinh nghiệm là gì. Chúng tôi chưa đề cập đến việc tối ưu hóa gas hoặc mã thông báo trong bài viết này, cả hai đều là những chủ đề quan trọng mà kiểm toán viên cần hiểu. Hãy tính toán và bạn sẽ thấy đây không phải là một hành trình ngắn.


Điều đó nói rằng, cộng đồng nói chung là thân thiện và hữu ích cho những người mới và có rất nhiều mẹo và thủ thuật. Nhưng đối với những người đọc bài viết này với hy vọng tạo dựng sự nghiệp từ bảo mật hợp đồng thông minh, điều quan trọng là phải hiểu rõ rằng tỷ lệ có được một sự nghiệp béo bở không có lợi cho bạn. Thành công không phải là kết quả mặc định.


Tất nhiên, nó có thể được thực hiện và khá nhiều người đã đi từ chỗ không biết gì về Solidity để có một sự nghiệp béo bở trong lĩnh vực kiểm toán. Có thể dễ dàng kiếm được công việc kiểm toán hợp đồng thông minh trong khoảng thời gian hai năm hơn là được nhận vào trường luật và vượt qua kỳ thi luật sư. Nó chắc chắn có nhiều ưu điểm hơn so với rất nhiều lựa chọn nghề nghiệp khác.


Tuy nhiên, nó sẽ đòi hỏi sự kiên trì phi thường từ phía bạn để nắm vững núi kiến thức phát triển nhanh chóng phía trước và trau dồi trực giác của bạn để phát hiện ra lỗi.

Điều này không có nghĩa là học bảo mật hợp đồng thông minh không phải là một mục tiêu đáng theo đuổi. Nó hoàn toàn là. Nhưng nếu bạn đang tiếp cận lĩnh vực này với các ký hiệu đô la trong mắt, hãy kiểm tra kỳ vọng của bạn.

Phần kết luận

Điều quan trọng là phải nhận thức được các anti-pattern đã biết. Tuy nhiên, hầu hết các lỗi trong thế giới thực đều dành riêng cho ứng dụng. Việc xác định một trong hai loại lỗ hổng đòi hỏi phải thực hành liên tục và có chủ ý.


Tìm hiểu bảo mật hợp đồng thông minh và nhiều chủ đề phát triển Ethereum khác với chương trình đào tạo về sự vững chắc hàng đầu trong ngành của chúng tôi.


Hình ảnh chính cho bài viết này được tạo bởiTrình tạo hình ảnh AI của HackerNoon thông qua lời nhắc "rô-bốt đang bảo vệ máy tính".