-
Notifications
You must be signed in to change notification settings - Fork 45
Solidity: By Examples
The below smart contracts demonstrate the Reentrancy Attack Scenario. Here from the Deposit contract, a low-level call is made to the attack contract, and from there the attack contract makes a continuous re-entry to the Deposit contract and drained out the fund.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DepositFunds {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
}
contract Attack {
DepositFunds public depositFunds;
constructor(address _depositFundsAddress) {
depositFunds = DepositFunds(_depositFundsAddress);
}
// Fallback is called when DepositFunds sends Ether to this contract.
fallback() external payable {
if (address(depositFunds).balance >= 1 ether) {
depositFunds.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
depositFunds.deposit{value: 1 ether}();
depositFunds.withdraw();
}
}
Here we can have a solution for this attack by using Check-Effects-Interaction pattern. In the withdraw(), just before making the transfer, update the balance of the withdrawer.
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
}
Circuit breakers stop execution if certain conditions are met, which can be useful when new errors are discovered. For example, most actions may be suspended in a contract if a bug is discovered, and the only action now active is a withdrawal. You can either give certain trusted parties the ability to trigger the circuit breaker or else have programmatic rules that automatically trigger certain breakers when certain conditions are met.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ExampleContract {
bool private stopped = false;
address private owner;
modifier isAdmin() {
require(msg.sender == owner);
_;
}
function toggleContractActive() isAdmin public {
// You can add an additional modifier that restricts stopping
// a contract to be based on another action, such as a vote of users
stopped = !stopped;
}
modifier stopInEmergency { if (!stopped) _; }
modifier onlyInEmergency { if (stopped) _; }
function deposit() stopInEmergency public {
// some code
}
function withdraw() onlyInEmergency public {
// some code
}
}
The calculation should be done using local variables and only the final result is stored in the state variable. This will eventually use up less gas. The following program demonstrates this fact.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MemoryCaching {
uint256 number = 1;
function getNumber() public {
for(uint8 i = 0; i < 100; i++) {
number += 1;
}
}
function justAddIt() public {
uint256 _number = number;
for(uint8 i = 0; i < 100; i++) {
_number += 1;
}
number = _number;
}
}
Several deployed smart contracts have been attacked, and millions of dollars have been stolen because of certain bugs or vulnerabilities in the contract code. Though smart contracts offer the benefit of immutability, upgrading the code may still be required. In traditional software, a software upgrade can introduce changes to any part of the software. But this is different in the case of smart contracts.
Upgradeability in smart contracts refers to the process of modifying smart contract code after deployment while maintaining the contract data and state. They offer two advantages:
- fixing security issues and bugs discovered post-deployment
- add new features and functionality in future.
An upgradeable smart contract is a contract that includes certain upgrading approaches proposed by the community and is designed specifically for possible upgrading. Since smart contracts are immutable, their code cannot be changed once deployed. However, the developer community has proposed several mechanisms to upgrade the contract, such as deploying a new smart contract and directing user requests to the new one instead of the previous one.
We will see in detail three main patterns used in upgradeable contracts.
This pattern requires the deployment of a new smart contract with updated code but with an empty state and migrating all states from the old contract to the new one. Since the contract address of the new contract is different, all the interactions with the old version must be updated to the new address.
Here, there are two contracts:
- storage contract, which preserves the states
- business logic contract, which has a code that needs to be modified.
End users interact with the logic contract, while the storage contract provides access to data only to authorized contract versions. In case of any upgrades, developers will deploy a new logic contract and then update the address specified as a variable in the storage contract. As all states are stored in the storage contract, there is no need for state migration.
In the proxy pattern, users interact with a proxy contract instead of the main business logic contract. The proxy contract stores data and forwards user requests to the targeted smart contract version. Whenever there is an upgrade, the admin deploys a new smart contract and specifies its address in the proxy contract as the target version.
The proxy contracts are often identified by the use of delegatecall. However, not all proxy contracts are upgradeable contracts. There can be proxy contracts which are used for redirecting transaction requests and not for upgrading a smart contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Box {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BoxV2 {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
// Increments the stored value by 1
function increment() public {
value = value + 1;
emit ValueChanged(value);
}
}
- Introduction
- Rise of Ethereum
- Ethereum Fundamentals
- DApps & Smart Contracts
- MetaMask Wallet & Ether
- Solidity: Basics
- Solidity: Advanced
- Solidity Use cases and Examples
- DApp Development: Introduction
- DApp Development: Contract
- DApp Development: Hardhat
- DApp Development: Server‐side Communication
- DApp Development: Client-side Communication
- Advanced DApp Concepts: Infura
- Advanced DApp Concepts: WalletConnect
- Event‐driven Testing
- Interacting with the Ethereum Network
- Tokens: Introduction
- Solidity: Best Practises
- Smart Contract Audit
- Ethereum: Advanced Concepts
- Evolution of Ethereum
- Conclusion