Â
Learning Objectives:
-
Master advanced Solidity features and patterns
-
Understand contract interactions and delegates
-
Learn about gas optimization and security patterns
4.3.1: Advanced Data Structures
Nested Mappings:
Nested Mapping Example:
contract MultiLevelMapping {
// Mapping of address to mapping of address to uint256
mapping(address => mapping(address => uint256)) public allowances;
function approve(address spender, uint256 amount) public {
allowances[msg.sender][spender] = amount;
}
function getAllowance(address owner, address spender)
public
view
returns (uint256)
{
return allowances[owner][spender];
}
}
// Three-level mapping
mapping(address => mapping(address => mapping(uint256 => bool))) public permissions;
// Usage:
// permissions[user][contract][actionId] = true/false
Custom Structs with Mappings:
Struct with Nested Mapping:
struct User {
string name;
uint256 balance;
mapping(uint256 => bool) permissions;
}
mapping(address => User) public users;
function addUser(address _address, string memory _name) public {
users[_address].name = _name;
}
function grantPermission(address _user, uint256 _permissionId) public {
users[_user].permissions[_permissionId] = true;
}
function hasPermission(address _user, uint256 _permissionId)
public
view
returns (bool)
{
return users[_user].permissions[_permissionId];
}
Arrays of Structs:
Struct Array:
struct Product {
uint256 id;
string name;
uint256 price;
}
Product[] public products;
uint256 public productCount;
function addProduct(string memory _name, uint256 _price) public {
productCount++;
products.push(Product(productCount, _name, _price));
}
function getProduct(uint256 _id) public view returns (Product memory) {
require(_id <= productCount, "Product not found");
return products[_id - 1]; // 1-indexed
}
4.3.2: Function Overloading
Function Overloading:
contract OverloadExample {
function process(uint256 _value) public pure returns (uint256) {
return _value * 2;
}
function process(string memory _value) public pure returns (string memory) {
return string.concat(_value, _value);
}
function process(address _addr) public view returns (uint256) {
return _addr.balance;
}
// Different number of parameters
function process(uint256 _a, uint256 _b) public pure returns (uint256) {
return _a + _b;
}
}
4.3.3: Fallback and Receive Functions
Receive Function:
Receive Function:
- Called when contract receives Ether without data
- Must be payable
- Cannot have arguments
- Cannot return anything
receive() external payable {
// Handle plain Ether transfers
emit Received(msg.sender, msg.value);
}
Fallback Function:
Fallback Function:
- Called when function doesn't exist
- Called when receive() doesn't exist
- Can be payable or non-payable
fallback() external payable {
// Handle unknown function calls
// Or forward to another contract
(bool success, ) = address(forwardTo).call(msg.data);
require(success, "Forward failed");
}
Complete Example:
Advanced Fallback/Receive:
contract AdvancedContract {
address public forwardTarget;
mapping(address => uint256) public balances;
event Received(address indexed sender, uint256 amount);
event Forwarded(address indexed target, bytes data);
constructor(address _target) {
forwardTarget = _target;
}
// Receive Ether
receive() external payable {
balances[msg.sender] += msg.value;
emit Received(msg.sender, msg.value);
}
// Fallback for function calls
fallback() external payable {
// If target is set, forward call
if (forwardTarget != address(0)) {
(bool success, ) = forwardTarget.call{value: msg.value}(msg.data);
require(success, "Forward failed");
emit Forwarded(forwardTarget, msg.data);
} else {
// Handle as deposit
balances[msg.sender] += msg.value;
emit Received(msg.sender, msg.value);
}
}
}
4.3.4: Contract Interactions
Calling Other Contracts:
Contract Interaction Methods: 1. Direct Call: - Interface defined - Type-safe - Recommended 2. Low-level Call: - More flexible - Gas control - Error handling 3. Delegate Call: - Context preserved - Storage in caller - Proxy pattern 4. Static Call: - View-only - No state changes - Gas efficient
Interface Implementation:
Interface Example:
interface IToken {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract TokenUser {
IToken public token;
constructor(address _token) {
token = IToken(_token);
}
function transferToken(address to, uint256 amount) public returns (bool) {
return token.transfer(to, amount);
}
function checkBalance(address account) public view returns (uint256) {
return token.balanceOf(account);
}
}
Low-Level Calls:
Low-Level Call Example:
contract CallExample {
address public target;
constructor(address _target) {
target = _target;
}
function callFunction(string memory _func, uint256 _value) public {
// Encode function call
bytes memory data = abi.encodeWithSignature(_func, _value);
// Call with gas limit and value
(bool success, bytes memory returnData) = target.call{gas: 100000, value: 0}(data);
require(success, "Call failed");
// Decode return value (if needed)
uint256 result = abi.decode(returnData, (uint256));
}
function callWithValue(address payable _target, uint256 _amount) public {
(bool success, ) = _target.call{value: _amount}("");
require(success, "Transfer failed");
}
}
Delegate Call (Proxy Pattern):
Delegate Call Example:
// Implementation contract
contract Implementation {
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
// Proxy contract
contract Proxy {
address public implementation;
uint256 public value; // Storage must match implementation
constructor(address _implementation) {
implementation = _implementation;
}
function setImplementation(address _implementation) public {
implementation = _implementation;
}
function setValue(uint256 _value) public {
// Delegate call to implementation
(bool success, ) = implementation.delegatecall(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success, "Delegate call failed");
}
}
4.3.5: Abstract Contracts and Interfaces
Abstract Contracts:
Abstract Contract:
- Contains incomplete implementation
- Cannot be deployed directly
- Must be inherited
abstract contract Animal {
function makeSound() public virtual returns (string memory);
function eat() public virtual {
// Common implementation
}
}
contract Dog is Animal {
function makeSound() public override returns (string memory) {
return "Woof!";
}
}
Interfaces:
Interface:
- No implementation
- Cannot have state variables
- Only function signatures
- No constructors
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address owner) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
4.3.6: Contract Factories
Factory Pattern:
Contract Factory:
contract ChildContract {
address public owner;
uint256 public number;
constructor(address _owner, uint256 _number) {
owner = _owner;
number = _number;
}
}
contract Factory {
ChildContract[] public children;
event ChildCreated(address indexed child, address indexed owner, uint256 number);
function createChild(uint256 _number) public {
ChildContract child = new ChildContract(msg.sender, _number);
children.push(child);
emit ChildCreated(address(child), msg.sender, _number);
}
function getChildrenCount() public view returns (uint256) {
return children.length;
}
}
CREATE2 (Deterministic Deployment):
CREATE2 Example:
contract Create2Example {
event Deployed(address indexed addr);
function deploy(bytes32 salt, uint256 number) public {
// Create bytecode
bytes memory bytecode = abi.encodePacked(
type(ChildContract).creationCode,
abi.encode(msg.sender, number)
);
address addr;
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
require(addr != address(0), "Creation failed");
emit Deployed(addr);
}
function predictAddress(bytes32 salt) public view returns (address) {
bytes memory bytecode = abi.encodePacked(
type(ChildContract).creationCode,
abi.encode(msg.sender, 0)
);
bytes32 hash = keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(bytecode)
));
return address(uint160(uint256(hash)));
}
}
4.3.7: Self-Destruct and Contract Lifecycle
Self-Destruct:
Self-Destruct Example:
contract Destructible {
address public owner;
constructor() {
owner = msg.sender;
}
function destroy(address payable _receiver) public {
require(msg.sender == owner, "Not owner");
selfdestruct(_receiver);
}
}
// Warning: After selfdestruct, the contract is removed
// Existing code and storage are removed
// ETH is sent to the receiver
// No way to interact with the contract anymore
Contract Lifecycle:
Contract States: 1. Creation: - Constructor executed - Initial state set - Contract deployed 2. Active: - Functions can be called - State can be modified - Events emitted 3. Self-Destructed: - Code and storage removed - ETH sent to specified address - No further interactions possible 4. Paused (Emergency): - Functions disabled - Funds frozen - Can be reactivated
4.3.8: Upgradable Contracts
Proxy Pattern (UUPS):
UUPS (Universal Upgradeable Proxy Standard):
// Implementation contract
contract ImplementationV1 {
uint256 public value;
function upgradeTo(address newImplementation) external virtual {
// UUPS upgrade logic
}
function setValue(uint256 _value) public virtual {
value = _value;
}
}
// Upgradeable Implementation
contract ImplementationV2 is ImplementationV1 {
uint256 public newVariable;
function setValue(uint256 _value) public override {
value = _value * 2; // New behavior
}
function setNewVariable(uint256 _value) public {
newVariable = _value;
}
}
// ERC-1967 Proxy
contract Proxy {
bytes32 private constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
constructor(address _implementation) {
_setImplementation(_implementation);
}
function _setImplementation(address _implementation) private {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
sstore(slot, _implementation)
}
}
fallback() external payable {
address impl;
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
impl := sload(slot)
}
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
4.3.9: Gas Optimization Patterns
Storage Packing:
Storage Packing Example:
contract StorageEfficient {
// Bad: 3 storage slots
uint256 a; // Slot 0
uint256 b; // Slot 1
uint256 c; // Slot 2
// Good: 2 storage slots (packed)
uint128 a; // Slot 0 (16 bytes)
uint128 b; // Slot 0 (16 bytes) - packed
uint256 c; // Slot 1
}
Memory vs Storage:
Memory vs Storage:
contract MemoryOptimization {
struct Data {
uint256 a;
uint256 b;
uint256 c;
}
Data[] public data;
// Bad: Copies to storage
function badUpdate(uint256 index) public {
Data memory temp = data[index]; // Copy from storage
temp.a = 1;
data[index] = temp; // Copy back to storage
}
// Good: Direct storage access
function goodUpdate(uint256 index) public {
Data storage temp = data[index]; // Reference to storage
temp.a = 1; // Direct modification
}
}
Short-Circuit Evaluation:
Short-Circuit:
function process(address user) public {
// Bad: Always executes both conditions
require(user != address(0) && user.balance > 0, "Invalid");
// Good: Checks cheaper condition first
require(user.balance > 0 && user != address(0), "Invalid");
}
ADDITIONAL DEEP TECHNICAL NOTES:
1. Solidity Assembly (Yul) Advanced
Advanced Assembly:
assembly {
// Memory management
let ptr := mload(0x40)
// Memory allocation
mstore(ptr, 0x12345678)
// Get free memory pointer
mstore(0x40, add(ptr, 0x20))
// Return data
return(ptr, 0x20)
// Revert with custom error
revert(ptr, 0x20)
}
2. Gas Optimization Tips
Advanced Gas Optimization:
1. Use immutable for constants:
uint256 public constant MAX = 100;
2. Use unchecked for safe operations:
unchecked { a = a + 1; }
3. Pack bools into uint256:
uint256 public flags; // Use bit operations
4. Use assembly for complex operations:
assembly {
result := add(a, b)
}
5. Avoid redundant calculations:
uint256 total = a + b + c; // One calculation
6. Use external functions over public:
function externalCall() external {}