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.
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 đó.
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.
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ý.
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ữ đó.
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.
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 .
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 ý.
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.
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.
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.
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.
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.
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” ở đâ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.
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.
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 ý.
Đâ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ạ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ó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.
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.
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() } }
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; } }
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; } } }
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 đó.
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.
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.
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.
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ể.
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.
Đâ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); } }
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.
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 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 }
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ữ.
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]; } }
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 đó.
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ú ý.
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); } }
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, 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ò).
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.
Solady SafeTransfer (tiết kiệm gas hơn đáng kể)
Đâ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.
(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.
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.
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.
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.
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 ủ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()")); } }
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/ ).
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…
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.
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 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.
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ú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.
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
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.
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
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.
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:
Chữ ký số có hai cách sử dụng trong bối cảnh hợp đồng thông minh:
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.
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
Khóa riêng (privKey) được sử dụng để tạo địa chỉ công khai (ethAddress)
Một hợp đồng thông minh lưu trữ ethAddress trước
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)
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.
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.
Đ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ý 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!
Đư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); } }
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.
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.
Phần 1: https://twitter.com/RareSkills_io/status/1650869999266037760
Phần 2: https://twitter.com/RareSkills_io/status/1650897671543197701
https://twitter.com/RareSkills_io/status/1651527648676573185 https://twitter.com/RareSkills_io/status/1651224817465540611
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.
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.
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.
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ữ.
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.
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 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?
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.
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.
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.
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.
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.
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.
Độ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.
Đ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/ ).
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.
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.
Đố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.
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.
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.
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
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.
Đó 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.
Đ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".