- It is not possible to fix the bug by updating the code in the smart contract due to the immutable nature of Ethereum smart contracts. Upgradable smart contracts are a way to overcome this issue for a certain limit. But, it reduces the user's trust in the smart contracts.
- There are several design techniques that can be considered while writing smart contracts to minimize the effect of a potential bug.
- Let's consider a simple contract that allows the deposit and withdrawal of Ether from a contract. We can try implementing various design techniques to avoid the loss of funds up to a certain limit:
pragma solidity ^0.4.24;
contract VulnerableContract {
function deposit() public {
// Code to accept Ether
}
function withdraw() public {
// Code to transfer Ether
}
}
- Include the rate limiting functionality in your contract, which will limit the task performed to a certain time period. For example, the contract will allow a user to withdraw only a certain amount of Ether/tokens per day. Additional withdrawals can be prevented completely or allowed only through elevated or multi-signature approval. This will greatly reduce the loss and give enough time for the user or owner to find a resolution.
- Model this contract in the following way to implement the functionality. The modifier verifies the time between the current and last withdrawal:
pragma solidity ^0.4.24;
contract ControlledContract {
// Simplified withdraw tracker
// Can include amount for more precise tracking
mapping(address => uint) lastWithdraw;
// Modifier to limit the rate of withdraw
modifier verifyWithdraw() {
require(lastWithdraw[msg.sender] + 1 days > now);
_;
}
function deposit() public {
// Code to accept Ether
}
function withdraw(uint _value) verifyWithdraw public {
// Code to transfer Ether
require(_value < 1 ether);
lastWithdraw[msg.sender] = now;
}
}
- Another approach is to pause or stop contract functionality as soon as a bug is discovered. This prevents the attacker from performing malicious actions on the contract. The example contract can be modified as follows to include the pause functionality:
pragma solidity ^0.4.24;
contract ControlledContract {
bool pause;
address owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
modifier whenNotPaused {
require(!pause);
_;
}
function pauseContract() onlyOwner public {
pause = true;
}
function unPauseContract() onlyOwner public {
pause = true;
}
function deposit() whenNotPaused public {
// some code
}
function withdraw() whenNotPaused public {
// some code
}
}
- Consider delaying contract actions as an approach in order to minimize the effect of an attack. Each contract action can either be performed after a certain time period or through an approval. The withdrawal contract can be further modified to include the mentioned method:
pragma solidity ^0.4.24;
contract ControlledContract {
struct WithdrawalReq {
uint value;
uint time;
}
mapping (address => uint) balances;
mapping (address => WithdrawalReq) requests;
uint constant delay = 7 days;
function requestWithdrawal() public {
require(balances[msg.sender] > 0);
uint amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;
requests[msg.sender] = WithdrawalReq(
amountToWithdraw,
now
);
}
function withdraw() public {
require(now > requests[msg.sender].time + delay);
uint amountToWithdraw = requests[msg.sender].value;
requests[msg.sender].value = 0;
msg.sender.transfer(amountToWithdraw);
}
}
- The decision on which design to choose depends heavily on the targeted user base and the requirements. You can either implement a fail-safe method or all of them. You can also come up with a fail-safe method based on your requirements, but make sure that the code is well-audited and error-free before deploying it to production.