# Overview Solidity là ngôn ngữ lập trình có cú pháp tương tự JS, nói chung dễ học dể triển khai # Development ở đâu Thưởng người ta dùng công cụ RemixIDE để code solidity: https://remix.ethereum.org/ Ví dụ. Paste đoạn code sau, compile rồi deploy ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract ExampleContract { function helloWorld() public pure returns (uint256) { return 100; } function haloDunia() public pure returns (bool) { return true; } } ``` Khi gọi hàm helloWorld hoặc haloDunia sẽ được value 100 và true ![](https://hackmd.io/_uploads/SyTjPZhxa.png) # Fixed size Datatype 3 kiểu dữ liệu phổ biến của Sol là: - uint256: là kiểu int có giới hạn tới 115792089237316195423570985008687907853269984665640564039457584007913129639935 - bool - address: lưu địa chỉ ví ``` // số lớn nhất trong python >>> pow(2,256) - 1 115792089237316195423570985008687907853269984665640564039457584007913129639935 } ``` # Testing Để run test thì có development framework `foundry` Cài đặt ``` # install foundry curl -L https://foundry.paradigm.xyz | bash # clone the practice problems git clone https://github.com/rareSkills/solidity-exercises.git # go to the first problem cd solidity-exercises/EverythingWorks # test the setup forge test ``` # Toán học trong Sol Khi thực hiện phép toán trong sol, các phép toán cộng, số mũ, chia lấy dư chạy như bình thg ``` uint256 sum = 10 + 5; // sum == 15 uint256 x = 1; uint256 y = 2; uint256 anotherSum = x + y; // anotherSum == 3 uint256 exp = 2 ** 3; // exp == 8 uint256 remainder = 10 % 4; // remainder == 2 ``` Nhưng Sol **không có float** (do foat ở các máy khác nhau sẽ làm tròn khác nhau => bất đồng thuận) Phép chia cũng khác fuck tạp, nếu muốn lấy 7,5% của 200 thì phải làm như sau ``` uint256 interest = 200 * 75 / 1000; ``` Còn nếu muốn biết % của tổng dân trong khu phố so với toàn thành phố thì cần làm cách khác chứ lấy 1000/1000000 thì nó ra đáp số = 0. Rõ bịp Sol không có số âm??? ``` // Đoạn code sau sẽ ko chạy nếu x=3, y=5 function subtract(uint256 x, uint256 y) public pure returns (uint256) { uint256 difference = x - y; return difference; } ``` # If/else trong Sol Nói chung if else, thậm chí là else if giống hệt những thằng khác. Nhưng so với Python đoạn code sau ko chạy ``` function isNotZero(uint256 x) public pure returns (bool) { if (x) { // will not compile return true; } else { return false; } } ``` Sol cũng không có **switch** # For loop cũng làm bình thường Sol cũng có **while** loops và **do while** loops nhưng mà ít dùng hơn. Sample ``` contract ExampleContract { function findPrimeFactor(uint256 x) public pure returns (uint256) { // start at 2, 1 is not a prime factor // use <= because x might be prime for(uint256 i = 2; i <= x; i++) { if (x % i == 0) { return i; } } } } ``` # Array và String ## Array Với Array cần chú ý vào calldata và memory ``` contract ExampleContract { function booleanArrayExample(bool[] calldata input) public pure returns (bool[] memory) { return input; } function addressArrayExample(address[] calldata input) public pure returns (address[] memory) { return address; } } ``` Nếu ko có calldata và memory thì code ko chạy ### What is calldata and memory? If you are familiar with C or C++, this concept will be intuitive. Memory in solidity is like the heap in C, C++, or Rust. Arrays can have unlimited size, so storing them on the execution stack (don’t worry if you don’t know what that is), could lead to a stackoverflow error (not to be confused with the famous forum!). Calldata is something unique to solidity. It is the actual “transaction data” that is sent when someone transmits a transaction to the blockchain. Calldata means “refer to the data in the Ethereum transaction itself.” This is a fairly advanced concept, so don’t worry if you don’t fully understand it for now. ### Lưu ý cho array Array cũng index từ 0 Để get length thì dùng .length Array có phép toán pop() Có thể khai báo fixed length cho array. Ví dụ ``` contract ExampleContract { function productOfarray(uint256[5] calldata myArray) public pure returns (uint256) { uint256 last = myArray[4]; return last; } } ``` ## Strings String tương tự array nhưng: - Không support nối string, nếu muốn nối thì đảm bảo parma lớn hơn 0.8.12 ``` pragma solidity ^0.8.12; contract ExampleContract { function useArrays(string calldata user) public pure returns(string memory) { return string.concat("hello ", user); } } ``` - String không index được => không thể run `myName[0]` - String không có length => cũng ko có `myName.length` - Khai báo string để sử dụng nhớ dùng calldata # Nested Array Dùng bình thg, giống các thằng khác ``` contract ExampleContract { // [[1,2],[3,4],[5,6]] becomes [1,2] function getRow(uint256[][] calldata nestedArray) public pure returns(uint256[] memory) { return nestedArray[0]; } } ``` # Biến toàn cục - Storage Variable Val khai báo bên ngoài function sẽ giữ giá trị sau khi transaction kết thúc. Ví dụ ``` contract ExampleContract { uint256 internal x; function setX(uint256 newValue) public { x = newValue; } function getX() public view returns (uint256) { return x; } } ``` Note: để ý cú pháp function là **view** hay **pure**, nếu sử dụng **pure** ở đây là func báo lỗi Cần nhớ 1 điều nữa **When a variable is declared public, it means other smart contracts can read the value but not modify it.** Ngoài ra các pure function không thể truy cập các Storage Val # Array in storage - Không thể pop 1 phần tử ở giữa mảng, phải pop và swap # Mapping Sử dụng để mappy key, value. Ví dụ ``` contract ExampleContract { mapping(uint256 => uint256) public myMapping; function setMapping(uint256 key, uint256 value) public { myMapping[key] = value; } function getValue(uint256 key) public view returns (uint256) { return myMapping[key]; } } ``` ## Lưu ý - Không thể khai báo mapping trong function - Mapping không thể for loop qua được - Mapping không thể return # Nested Mapping Ko biết nói gì. Ví dụ ``` contract ExampleContract { mapping(uint256 => mapping(uint256 => uint256)) public nestedMap; function setNestedMap(uint256 key1, uint256 key2, uint256 finalValue) public { nestedMap[key1][key2] = finalValue; } function getNestedMap(uint256 key1, uint256 key2, uint256 finalValue) public { return nestedMap[key1][key2]; } } ``` > Chú ý: Public Nested Mappings Don’t Work ???? # msg.sender và address(this) **msg.sender** trả về address của người đang call tới func của SC Zí dụ: ``` contract ExampleContract { function whoami() public view returns (address) { address sender = msg.sender; return sender; } } ``` **tx.origin** có cơ chế giống msg.sender nhưng mà nó có vấn đề bảo mật, tránh sử dụng tạm đã **address(this)** lấy ra own address là người deploy SM # Constructor ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract ExampleContract { address public banker; constructor(address _banker) { banker = _banker; } function getOwn() public view returns (address) { return address(this); } } ``` > Cần nhớ: constructors cannot return values. # Required Sử dụng từ khóa required khi muốn validate điều kiện trong transaction, khi không pass các điều kiện này giao dịch sẽ bị revert. Tốt nhất là để note rõ ràng vào để dễ debug sau này Ví dụ ``` contract ExampleContract { function mustNotBeFive(uint256 x) public pure returns (uint256) { require(x != 5, "five is not valid"); return x * 2; } } ``` # ERC20 Token Ban đầu cần quay lại bản chất của Token, đó là việc tạo ra một smart contract (hợp đồng thông minh) tuân theo các chuẩn token như ERC-20, BEP-20, SPL,... Tham khảo chuẩn ERC-20 https://eips.ethereum.org/EIPS/eip-20 Đoạn code sau là 1 SC hoàn chỉnh để sử dụng token ``` contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint8 public decimals; uint256 public totalSupply; // owner -> spender -> allowance // this enables an owner to give allowance to multiple addresses mapping(address => mapping(address => uint256)) public allowance; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; decimals = 18; owner = msg.sender; } function mint(address to, uint256 amount) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } function transfer(address to, uint256 amount) public returns (bool) { return helperTransfer(msg.sender, to, amount); } function approve(address spender, uint256 amount) public returns (bool) { allowance[msg.sender][spender] = amount; return true; } // just added function transferFrom(address from, address to, uint256 amount) public returns (bool) { if (msg.sender != from) { require(allowance[from][msg.sender] >= amount, "not enough allowance"); allowance[from][msg.sender] -= amount; } return helperTransfer(from, to, amount); } // it's very important for this function to be internal! function helperTransfer(address from, address to, uint256 amount) internal returns (bool) { require(balanceOf[from] >= amount, "not enough money"); require(to != address(0), "cannot send to address(0)"); balanceOf[from] -= amount; balanceOf[to] += amount; return true; } } ``` # Tuples Lưu ý rằng keyword tuples không tồn tại trong Sol, tuy nhiên Sol có kiểu dữ liệu giống như tuple trong Python. Ví dụ ``` contract ExampleContract { function getTopLeaderboardScore() public pure returns (address, uint256) { return (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 100); } } ``` Có thể unpack để get các item bên trong tuple, độ dài tuple là tùy ý Ví dụ **unpack** tuple ``` contract ExampleContract { function highestScoreIsOver9000() public pure returns (bool) { (address leader, uint256 score) = (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 100); if (score > 9000) { return true; } return false; } } ``` # ABI Encoding Trước hết chú ý vào input trong mỗi transaction, ở đây input có dạng: `0xf8689fd30000000000000000000000000000000000000000000000000000000000000007` ![](https://hackmd.io/_uploads/rJS2lBTgp.png) Với ý nghĩa là f8689fd3 tương ứng với việc call function `takeOneArg` và truyền đầu vào là `0000000000000000000000000000000000000000000000000000000000000007` Đó là ví dụ của abi encode. Vì sao lại phải **endcode**, lý do là để xử lý và truyền tải các dữ liệu phức tạp trong Ethereum một cách hiệu quả và tối ưu về gas fee. --- Xét thêm đến một ví dụ khác ``` contract ExampleContract{ function encodingXY(bool x, uint256 y) public pure returns (bytes memory) { return abi.encode(x,y); } function getATuple(bytes memory encoding) public pure returns (bool, uint256) { (bool x, uint256 y) = abi.decode(encoding, (bool, uint256)); return(x,y); } } ``` Trong đó - `encodingXY` trả về dữ liệu kiểu bytes memory là endcode của 2 biến uint256 x và y - `getATuple` nhận đầu vào là bytes memory và sau khi abi.decode, nó trả ra giá trị thực của x và y > Tóm lại ABI là cách để mã hóa 2 chiều và truyền tải dữ liệu dễ dàng hơn, các số thập phân sau khi endcode sẽ được quy đổi sang hex > > Nó cũng có nhược điểm là: chỉ hoạt động với các contract, không phải tất cả các ngôn ngữ lập trình đều decode được ABI. # Calling other contracts Sol không cho phép chỉ định function là view nếu func đó gọi một SC khác => Vì làm sao biết được SC khác chỉ thực hiện view? Ta sử dụng đoạn code sau để call function khác ``` source.call(abi.encodeWithSignature("meaningOfLifeAndAllExistence()")); ``` Với meaningOfLifeAndAllExistence là tên func của SC tương ứng -- Nhớ revert vì rất có thể func mà ta call đến nó return null -- Cuối cùng, để có thể truyền đối số vào func của SC khác, ta sử dụng ``` (bool ok, bytes memory result) = source.call(abi.encodeWithSignature("add(uint256,uint256)", x, y)); ``` Cần nhớ: *không có space trong cụm "add(uint256,uint256)"* Ví dụ ``` contract ExampleContract { function callAdd(address source, uint256 x, uint256 y) public returns (uint256) { (bool ok, bytes memory result) = source.call(abi.encodeWithSignature("add(uint256,uint256)", x, y)); require(ok, "call failed"); uint256 sum = abi.decode(result, (uint256)); return sum; } } contract Calc { function add(uint256 x, uint256 y) public returns (uint256) { return x + y; } } ``` # Payable payable được sử dụng để chỉ định rằng một hàm hoặc địa chỉ có thể nhận Ether trực tiếp, nếu không có payable thì sẽ không gửi ether vào SC được. Có thể đặt payable vào constructor() của SC để thêm ether vào SC trong lúc deploy. Ngoài ra có một cách khác là để payable vào 1 func nào đó trong SC, như vậy SC cũng có thể nhận được ether Để get balance ether của SC ta sử dụng `address(this).balance;` Ví dụ: ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract ReceiveEther { // khi khởi tạo thì cho phép nhận ether từ ví deploy SC // có thể nhận ether khi call func takeMoney() constructor() payable {} function takeMoney() public payable {} function myBalance() public view returns (uint256) { return address(this).balance; } } contract SendMoney { // khi khởi tạo thì cho phép nhận ether từ ví deploy SC // lúc call func sendMoney thì nó lấy hết ether gửi đi sang destAddr constructor() payable {} function sendMoney(address receiveEtherContract) public payable { uint256 amount = myBalance(); (bool ok, ) = receiveEtherContract.call{value: amount}( abi.encodeWithSignature("takeMoney()") ); require(ok, "transfer failed"); } function myBalance() public view returns (uint256) { return address(this).balance; } } ``` **Một vài lưu ý:** > Payable functions cannot be view or pure # Receive là một func đặc biệt để cho phép nhận ether (na ná như constructor), khi một SC khác muốn chuyển ether cho SC này, chỉ cần gọi tới `destAddress.call{value: 10}("");` Nhớ thêm payable vào func này Ví dụ ``` contract TakeMoney { receive() external payable { } function viewBalance() public view returns (uint256) { return address(this).balance; } } contract ForwardMoney { function payMe() public payable { } function sendMoney(address luckyAddress) public payable { uint256 myBalance = viewBalance(); luckyAddress.call{value: myBalance}(""); } function viewBalance() public view returns (uint256) { return address(this).balance; } } ``` # block.timestamp và block.number là biến toàn cục trả về thời gian của block hiện tại tính theo giây, và trả về số block hiện tại trên blockchain. Các ứng dụng của block.timestamp và block.number: * Xác định thời gian tạo smart contract, thời gian giao dịch. * Xác định tuổi tác của block trong code. * Tạo seed ngẫu nhiên dựa trên block hash. * Các tính năng mã hóa thời gian (timelock) trong smart contract. # Event Ví dụ về việc xem event ở https://goerli.etherscan.io/address/0xAF5e2CC26205ff6F33ed0E6e2888e8EF7d2a3559#events # Inheritance - thừa kế Sol hỗ trợ kế thừa, tuy nhiên có những rule như sau * Only virtual functions can be overriden * Functions that override a parent’s function must have an override modifier * The overriding function must match exactly, in name, arguments, and return type * Instead of copying and pasting the parent function’s code, you can use the super keyword * You can inherit from multiple contracts * You must explicitly call a parent’s constructor when doing inheritance. ## Tạo token ERC20 nhanh bằng cách kế thừ lib có sẵn ``` import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract Token is ERC20("SomeToken", "symbol") { } ```