乐闻世界logo
搜索文章和话题

How to use Assembly for low-level optimization in Solidity? What are the precautions?

3月6日 23:07

Assembly is a low-level programming method in Solidity that allows developers to directly manipulate EVM bytecode. While it provides extreme flexibility and Gas optimization space, it also increases code complexity and security risks.

1. Why Use Assembly

solidity
contract AssemblyBenefits { // 1. Gas optimization: Skip Solidity's high-level abstractions // 2. Access to low-level EVM features // 3. Implement operations not supported by Solidity // 4. Fine-grained control of memory and storage }

2. Assembly Basic Syntax

Inline Assembly

solidity
contract BasicAssembly { // Use assembly keyword function add(uint256 a, uint256 b) external pure returns (uint256 result) { assembly { // Directly use EVM opcodes result := add(a, b) // Addition operation } } // Multiple operations function calculate(uint256 x) external pure returns (uint256) { assembly { let y := add(x, 10) // Local variable let z := mul(y, 2) // Multiplication mstore(0x00, z) // Store to memory return(0x00, 32) // Return value from memory } } }

Data Types and Operations

solidity
contract AssemblyDataTypes { // Basic type operations function dataTypes() external pure { assembly { // Integers let a := 100 let b := 0xFF // Hexadecimal // Boolean (0 or 1) let flag := 1 // Address let addr := 0x1234567890123456789012345678901234567890 // Arithmetic operations let sum := add(a, b) let diff := sub(a, b) let prod := mul(a, b) let quot := div(a, b) let rem := mod(a, b) // Bitwise operations let andResult := and(a, b) let orResult := or(a, b) let xorResult := xor(a, b) let notResult := not(a) let shifted := shl(2, a) // Left shift let shiftedR := shr(2, a) // Right shift } } }

3. Memory Operations

Memory Layout

solidity
contract MemoryOperations { /* Memory layout: 0x00 - 0x3f (64 bytes): Temporary space for hash functions 0x40 - 0x5f (32 bytes): Currently allocated memory size (free memory pointer) 0x60 - ...: Actual data storage */ function memoryBasics() external pure returns (uint256) { assembly { // Read free memory pointer let freePtr := mload(0x40) // Store data at free memory location mstore(freePtr, 12345) // Update free memory pointer (32 bytes = 0x20) mstore(0x40, add(freePtr, 0x20)) // Read memory let value := mload(freePtr) mstore(0x00, value) return(0x00, 32) } } // Store multiple values function storeMultiple() external pure returns (uint256, uint256) { assembly { let freePtr := mload(0x40) // Store multiple 32-byte values mstore(freePtr, 100) mstore(add(freePtr, 0x20), 200) mstore(add(freePtr, 0x40), 300) // Update free memory pointer mstore(0x40, add(freePtr, 0x60)) // Return first two values mstore(0x00, mload(freePtr)) mstore(0x20, mload(add(freePtr, 0x20))) return(0x00, 64) } } // Byte operations function byteOperations() external pure returns (bytes32) { assembly { let value := 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef // Extract single byte (from right to left, 0-31) let byte0 := byte(31, value) // Last byte let byte1 := byte(30, value) // Second to last byte mstore(0x00, byte0) return(0x00, 32) } } }

4. Storage Operations

Storage Layout

solidity
contract StorageOperations { uint256 public value1; // slot 0 uint256 public value2; // slot 1 mapping(address => uint256) public balances; // slot 2 address public owner; // slot 3 function storageRead() external view returns (uint256) { assembly { // Read value at slot 0 let v := sload(0) mstore(0x00, v) return(0x00, 32) } } function storageWrite(uint256 _value) external { assembly { // Write to slot 0 sstore(0, _value) } } // Read mapping function readMapping(address _key) external view returns (uint256) { assembly { // Mapping storage position calculation: keccak256(key . slot) mstore(0x00, _key) mstore(0x20, 2) // balances is at slot 2 let slot := keccak256(0x00, 0x40) let value := sload(slot) mstore(0x00, value) return(0x00, 32) } } // Write mapping function writeMapping(address _key, uint256 _value) external { assembly { mstore(0x00, _key) mstore(0x20, 2) let slot := keccak256(0x00, 0x40) sstore(slot, _value) } } // Read dynamic array uint256[] public dynamicArray; // slot 4 function readArrayLength() external view returns (uint256) { assembly { // Dynamic array length stored at declared slot let length := sload(4) mstore(0x00, length) return(0x00, 32) } } function readArrayElement(uint256 _index) external view returns (uint256) { assembly { // Array element storage position: keccak256(slot) + index mstore(0x00, 4) let baseSlot := keccak256(0x00, 0x20) let elementSlot := add(baseSlot, _index) let value := sload(elementSlot) mstore(0x00, value) return(0x00, 32) } } }

5. Function Calls and Returns

External Calls

solidity
contract ExternalCalls { function callExternal(address _target, bytes memory _data) external returns (bool success, bytes memory result) { assembly { // Get data location and size let dataPtr := add(_data, 0x20) // Skip length field let dataSize := mload(_data) // Allocate memory for return data let resultPtr := mload(0x40) // Execute call success := call( gas(), // Remaining Gas _target, // Target address 0, // ETH to send dataPtr, // Input data location dataSize, // Input data size resultPtr, // Return data location 0x40 // Return data size limit ) // Get return data size let resultSize := returndatasize() // Copy return data returndatacopy(resultPtr, 0, resultSize) // Update free memory pointer mstore(0x40, add(resultPtr, resultSize)) // Set result pointer and length mstore(result, resultSize) mstore(add(result, 0x20), resultPtr) } } // Send ETH function sendETH(address _to, uint256 _amount) external returns (bool) { assembly { // Use call to send ETH let success := call( gas(), _to, _amount, // Amount of ETH to send 0, // No input data 0, 0, // Don't care about return data 0 ) mstore(0x00, success) return(0x00, 32) } } // Static call (doesn't modify state) function staticCall(address _target, bytes memory _data) external view returns (bool success, bytes memory result) { assembly { let dataPtr := add(_data, 0x20) let dataSize := mload(_data) let resultPtr := mload(0x40) success := staticcall( gas(), _target, dataPtr, dataSize, resultPtr, 0x40 ) let resultSize := returndatasize() returndatacopy(resultPtr, 0, resultSize) mstore(0x40, add(resultPtr, resultSize)) mstore(result, resultSize) mstore(add(result, 0x20), resultPtr) } } }

Delegate Call

solidity
contract DelegateCallExample { address public implementation; uint256 public value; function delegateToImplementation(bytes memory _data) external returns (bytes memory) { assembly { let dataPtr := add(_data, 0x20) let dataSize := mload(_data) let resultPtr := mload(0x40) // Use delegatecall let success := delegatecall( gas(), sload(0), // implementation address at slot 0 dataPtr, dataSize, resultPtr, 0x40 ) let resultSize := returndatasize() returndatacopy(resultPtr, 0, resultSize) // Check if call was successful if iszero(success) { revert(resultPtr, resultSize) } return(resultPtr, resultSize) } } }

6. Gas Optimization Techniques

Common Optimization Patterns

solidity
contract GasOptimizations { uint256[] public items; // Before optimization: Use Solidity high-level syntax function sumSolidity() external view returns (uint256 total) { for (uint i = 0; i < items.length; i++) { total += items[i]; } } // After optimization: Use Assembly function sumAssembly() external view returns (uint256 total) { assembly { // Get array storage location mstore(0x00, items.slot) let baseSlot := keccak256(0x00, 0x20) // Get array length let length := sload(items.slot) // Iterate through array for { let i := 0 } lt(i, length) { i := add(i, 1) } { let slot := add(baseSlot, i) let value := sload(slot) total := add(total, value) } mstore(0x00, total) return(0x00, 32) } } // Batch operation optimization function batchTransfer(address[] memory _recipients, uint256[] memory _amounts) external payable { require(_recipients.length == _amounts.length, "Length mismatch"); assembly { let recipientsPtr := add(_recipients, 0x20) let amountsPtr := add(_amounts, 0x20) let length := mload(_recipients) for { let i := 0 } lt(i, length) { i := add(i, 1) } { let recipient := mload(add(recipientsPtr, mul(i, 0x20))) let amount := mload(add(amountsPtr, mul(i, 0x20))) // Use call to send ETH let success := call( gas(), recipient, amount, 0, 0, 0, 0 ) if iszero(success) { revert(0, 0) } } } } // Short-circuit sum optimization function optimizedSum(uint256[] memory _values) external pure returns (uint256) { assembly { let ptr := add(_values, 0x20) let length := mload(_values) let total := 0 // Use unchecked addition (Solidity 0.8.0+) for { let i := 0 } lt(i, length) { i := add(i, 1) } { total := add(total, mload(add(ptr, mul(i, 0x20)))) } mstore(0x00, total) return(0x00, 32) } } }

7. Advanced Techniques

Contract Creation

solidity
contract ContractCreation { function deploy(bytes memory _bytecode) external returns (address addr) { assembly { let size := mload(_bytecode) let ptr := add(_bytecode, 0x20) // Use create to deploy contract addr := create(0, ptr, size) // Check if deployment was successful if iszero(extcodesize(addr)) { revert(0, 0) } } } // Use create2 for deployment (deterministic address) function deployWithSalt(bytes memory _bytecode, bytes32 _salt) external returns (address addr) { assembly { let size := mload(_bytecode) let ptr := add(_bytecode, 0x20) addr := create2(0, ptr, size, _salt) if iszero(extcodesize(addr)) { revert(0, 0) } } } // Calculate create2 address function computeAddress( bytes32 _salt, bytes32 _bytecodeHash, address _deployer ) external pure returns (address) { assembly { // Address = last 20 bytes of keccak256(0xff + deployer + salt + bytecodeHash) mstore(0x00, _deployer) mstore(0x20, _salt) mstore(0x40, _bytecodeHash) let hash := keccak256(0x00, 0x60) mstore(0x00, hash) return(12, 20) // Return last 20 bytes } } }

Inline Assembly with Solidity Interaction

solidity
contract AssemblyInteraction { struct User { uint256 id; address wallet; uint256 balance; } mapping(uint256 => User) public users; // Use Assembly to read struct function getUserAssembly(uint256 _id) external view returns (User memory user) { assembly { // Calculate mapping storage location mstore(0x00, _id) mstore(0x20, users.slot) let baseSlot := keccak256(0x00, 0x40) // Read each field of struct // User struct occupies 3 slots let id := sload(baseSlot) let wallet := sload(add(baseSlot, 1)) let balance := sload(add(baseSlot, 2)) // Store to memory for return let ptr := mload(0x40) mstore(ptr, id) mstore(add(ptr, 0x20), wallet) mstore(add(ptr, 0x40), balance) // Update free memory pointer mstore(0x40, add(ptr, 0x60)) // Set return value mstore(user, 0x60) // Point to memory location mstore(add(user, 0x20), ptr) } } // Conditional optimization function optimizedCondition(uint256 x) external pure returns (uint256) { assembly { // Use switch for multi-condition judgment switch x case 0 { mstore(0x00, 100) } case 1 { mstore(0x00, 200) } default { mstore(0x00, 300) } return(0x00, 32) } } }

8. Safety Considerations

solidity
contract AssemblySafety { /* Safety considerations: 1. Memory management: Ensure correct update of free memory pointer 2. Storage conflicts: Avoid overwriting important storage slots 3. Overflow checks: Assembly does not automatically check overflow 4. Call validation: Check external call return values 5. Reentrancy protection: Manually implement reentrancy lock */ bool private locked; modifier noReentrant() { require(!locked, "Reentrant call"); locked = true; _; locked = false; } // Safe ETH transfer function safeTransferETH(address _to, uint256 _amount) external noReentrant { assembly { // Check if address is valid if iszero(_to) { revert(0, 0) } // Execute transfer let success := call( gas(), _to, _amount, 0, 0, 0, 0 ) // Verify result if iszero(success) { revert(0, 0) } } } // Prevent integer overflow function safeAdd(uint256 a, uint256 b) external pure returns (uint256) { assembly { let result := add(a, b) // Check overflow: if result < a, overflow occurred if lt(result, a) { revert(0, 0) } mstore(0x00, result) return(0x00, 32) } } // Safe storage write function safeStorageWrite(uint256 slot, uint256 value) external { assembly { // Check if slot is within safe range if lt(slot, 100) { revert(0, 0) } sstore(slot, value) } } }

9. Common Use Cases

solidity
contract AssemblyUseCases { // 1. Efficient hash calculation function efficientHash(bytes memory _data) external pure returns (bytes32) { assembly { let ptr := add(_data, 0x20) let size := mload(_data) let hash := keccak256(ptr, size) mstore(0x00, hash) return(0x00, 32) } } // 2. Check contract code function hasCode(address _addr) external view returns (bool) { assembly { let size := extcodesize(_addr) mstore(0x00, gt(size, 0)) return(0x00, 32) } } // 3. Get current contract address function currentAddress() external view returns (address) { assembly { mstore(0x00, address()) return(0x00, 32) } } // 4. Get caller function getCaller() external view returns (address) { assembly { mstore(0x00, caller()) return(0x00, 32) } } // 5. Get call data function getCallData() external pure returns (bytes32) { assembly { // Get function selector (first 4 bytes) let selector := calldataload(0) mstore(0x00, selector) return(0x00, 32) } } // 6. Efficient array operations function findAndRemove(uint256[] storage _arr, uint256 _value) external { assembly { // Get array length let length := sload(_arr.slot) // Find element mstore(0x00, _arr.slot) let baseSlot := keccak256(0x00, 0x20) for { let i := 0 } lt(i, length) { i := add(i, 1) } { let slot := add(baseSlot, i) if eq(sload(slot), _value) { // Found element, replace with last element let lastSlot := add(baseSlot, sub(length, 1)) sstore(slot, sload(lastSlot)) // Decrease length sstore(_arr.slot, sub(length, 1)) // Stop loop stop() } } } } }

10. Summary

Assembly is a powerful low-level tool but needs to be used with caution:

  1. Use Cases:

    • Gas-sensitive critical paths
    • Implementing features not supported by Solidity
    • Fine-grained control of memory and storage
  2. Best Practices:

    • Only use Assembly when necessary
    • Test and audit thoroughly
    • Add detailed comments
    • Consider using Yul (safer assembly dialect)
  3. Security Points:

    • Correctly manage memory pointer
    • Verify all external calls
    • Manually check overflow
    • Avoid overwriting critical storage
  4. Performance Trade-offs:

    • Assembly can significantly save Gas
    • But increases code complexity and maintenance cost
    • Need to balance readability and performance
标签:Solidity