Last active
April 29, 2022 23:17
-
-
Save cwli24/354120a627087ffab029ea50ee09753e to your computer and use it in GitHub Desktop.
Quick reference to Solidity syntax (vers. 0.8)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.7; // '^' is for >= this version | |
/* DISCLAIMER: | |
This is a sample that provides a brief overview and glimpse into the basics of Solidity 0.8 types and language structure. | |
It is a quick breakdown meant for those with previous coding experience. All the highlights and key points are noted as comments. | |
With the economics and scalability-congestion of ETH in mind, search for "GAS" tips that help lower the cost of your contract executions. | |
*/ | |
contract HelloWorld { | |
string public myString = "hello world"; | |
// value types: default of bool is 'false', rest is '0' or '0x0000...' | |
bool public b = true; | |
uint public u = 123; // uint = uint256, also uint8 and uint16 avail | |
int public i = -123; // also int128 | |
int public minInt = type(int).min; // -2**255 -- 0.8 features safe math as overflow or underflow throws error | |
int public maxInt = type(int).max; // 2**255-1 | |
address public addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; | |
//bytes32 public b32 = ...; // some keccak256 SHA-3 digest | |
address public constant MY_ADDR = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2; // consts save a bit of GAS | |
function add(uint x, uint y) external pure returns (uint) { // 'pure' => read only fn, doesn't modify outside data | |
return x + y; | |
} | |
uint public stateVariable = 123; // inside contract, outside function(s) | |
function foo() external { | |
uint notStateVariable = 456; // inside functions => local vars | |
} | |
function globalVars() external view returns (address, uint, uint) { // unlike 'pure', 'view' fn can also read data from state/global vars | |
address sender = msg.sender; | |
uint timestamp = block.timestamp; | |
uint blockNum = block.number; | |
return (sender, timestamp, blockNum); | |
} | |
// all 3 of these will: refund gas, revert state updates | |
function testRequireRevertAssert(uint _i) public view { | |
require(_i <= 10, "error: i > 10"); | |
revert("error"); | |
assert(i == 10); // requires 'view' | |
} | |
error MyError(address caller, uint i); | |
function testCustomError(uint _i) public view { | |
revert MyError(msg.sender, _i); // custom errors saves GAS! | |
} | |
bool public paused; | |
modifier whenNotPaused() { | |
require(!paused, "paused"); // pro tip: putting this inside *another internal* fn can save GAS! | |
_; // tells solidity to call the actual fn that this wraps | |
// can call more code here, "sandwich" | |
} | |
function inc() external whenNotPaused { // can chain modifiers! | |
b = false; | |
} | |
function returnMany() public pure returns (uint x, bool b) { | |
x = 1; | |
b = true; // assigning outputs directly saves a bit of GAS | |
} | |
function destructingAssignments() public pure { | |
(uint x, bool b) = returnMany(); | |
(, bool _b) = returnMany(); | |
} | |
uint[] public dynamicArray = [42]; | |
uint[3] public fixedArray; | |
mapping(address => mapping(address => bool)) public isFriend; | |
function examples() external returns (uint[] memory) { // returning array is generally not recommended -- can burn GAS | |
dynamicArray.push(69); // [42, 69] | |
delete dynamicArray[0]; // [0, 69] | |
dynamicArray.pop(); // [0] | |
uint[] memory a = new uint[](3); // array in memory can only be fixed size | |
isFriend[msg.sender][address(1)] = true; | |
return a; | |
} | |
/* There are 3 data areas: storage | memory | stack. You can think of these as disk | RAM heap | RAM stack, respectively, in terms | |
of cost or speed of access and space. See https://stackoverflow.com/a/33839164 for details on the data location of types. | |
*/ | |
struct Car{ | |
string model; | |
uint year; | |
address owner; | |
} | |
Car[] public cars; | |
function examples2(Car[] calldata y) external { | |
Car memory toyota = Car("Toyota", 1990, msg.sender); | |
cars.push(toyota); | |
cars.push(Car({year: 1980, model: "Lambo", owner: msg.sender})); | |
Car storage _car = cars[0]; // if use 'memory', modifications would not be saved after fn returns | |
_car.year = 1999; | |
delete cars[1]; | |
cars[1] = y[0]; // 'calldata' is read-only but can save GAS b/c passing it forward into inner fns, it won't be copied again | |
} | |
/* - event parameters are kept in transaction logs on the blockchain, not within smart contract | |
- params are placed in either "data" or "topics" | |
- events can be filtered by *name* and by *contract address* | |
- logs are tied by contract addr and stay in the blockchain permanently, contingent on block accessibility | |
- 'indexed' params appear in "topics" portion; those without are ABI-encoded into data portion | |
*/ | |
event Message(address indexed _from, address indexed _to, string message); // only up to 3 params can be indexed, even if there are 4+ | |
function sendMessage(address _to, string calldata message) external { // strings are dynamic type so 'calldata' works | |
emit Message(msg.sender, _to, message); | |
} | |
} | |
/* ---Inheritance--- */ | |
contract A { | |
function foo() public pure virtual returns (string memory) { | |
return "A"; // ^reference types (struct, array or mapping) including string must be made explicit | |
} // on where their data is stored. | |
function bar() public pure returns (string memory) { | |
return "A"; | |
} | |
} | |
contract B is A { | |
function foo() public pure virtual override returns (string memory){ // need both 'virtual' (for C) and 'override' (for A) | |
return "B"; | |
} | |
} | |
// multiple inheritance must be listed in order from most base-like to derived (inheritance level) | |
contract C is A, B { | |
function foo() public pure override(A, B) returns (string memory){ | |
return "C"; | |
} | |
} | |
contract X { | |
string public name; | |
constructor(string memory _name){ // constructor parameters cannot be 'calldata' | |
name = _name; | |
} | |
event Logx(string message); | |
function sharedFunction() public virtual { | |
emit Logx("X.GonGiveItToYa"); | |
} | |
} | |
contract Y { | |
string public text; | |
constructor(string memory _text) { | |
text = _text; | |
} | |
event Logy(string message); | |
function sharedFunction() public virtual { | |
emit Logy("y.YaGottaBeSoRude"); | |
} | |
} | |
contract Z is X('x'), Y('y') { // one way to call the base constructor(s) -- the order here determines order that constructors are called! | |
constructor(string memory _name, string memory _text) /* X('x') Y('y') */{ // another way to call base constructor(s) | |
// can also use a combination of the 2 above methods | |
/* note: if base class defines a constructor, its derived class(es) MUST also define their own or throws 'abstract' compiler error | |
for more information on this and "interface" vs "abstract" vs "contract", see: https://ethereum.stackexchange.com/a/83270 | |
*/ | |
} | |
function sharedFunction() public override(X, Y) { | |
Y.sharedFunction(); // calling a parent fn directly | |
super.sharedFunction(); // this will call BOTH (or all) parents' sharedFunction() | |
} | |
} | |
/* Visibility keywords (in variables & functions): | |
'private' - only accessible inside contract | |
'internal' - only inside contract and by child(derived) contracts <-- DEFAULT SCOPE | |
'public' - inside and outside contract | |
'external' - only from outside contract | |
> inside a contract, you can however use "this.externalFunc()" to loop-call but it's a hack and GAS inefficient so don't. | |
*/ | |
contract HelloWorldSender { | |
address payable public immutable owner; // immutable is like constant but is assigned at construction time, whereas latter is at compile time | |
constructor() payable { // contract constructor, if defined, must also be marked 'payable' to send | |
owner = payable(msg.sender); // address has to be 'payable' type to send ETH | |
} | |
function deposit() external payable {} // function has to be 'payable' to receive ETH | |
fallback() external payable {} // special fn executed when a non-existing fn of this contract is called; mainly used to enable receiving ETH | |
receive() external payable {} // just a niche fn called when 'msg.data' is empty -- if this isn't defined, it'll just go to fallback() | |
function sendByTransfer(address payable to) external payable { | |
to.transfer(123); // if this fails, whole txn fails and reverts | |
} | |
function sendBySend(address payable to) external payable { | |
bool sent = to.send(123); | |
require(sent, "send failed"); // Send is not really used in practice, mainly the other 2 ways | |
} | |
// ^ transfer() & send() forwards 2300 gas | |
function sendByCall(address payable to) external payable { | |
(bool success, /*bytes memory data*/) = to.call{value: 123}(""); | |
require(success, "call failed"); | |
} // ^ forwards all gas avail by default (save 1/64th by EIP-150), unless specified | |
function setXandSendEther(address payable _test, uint _x) external payable { | |
ByeWorldReceiver(_test).setXandReceiveEther{value: msg.value}(_x); // initialize other contract, call its fn, and send ether | |
// _test can be of type 'ByeWorldReceiver' too instead of 'address' | |
} | |
} | |
contract ByeWorldReceiver { | |
event Log(uint amount, uint gas); | |
receive() external payable { | |
emit Log(msg.value, gasleft()); // amt of ether & amt of gas that was sent | |
} | |
uint public x; | |
uint public value; | |
function setXandReceiveEther(uint _x) external payable { | |
x = _x*2; | |
value = msg.value; | |
} | |
function addValue(uint val) external { | |
value += val; | |
} | |
} | |
// Say we don't know anything about ByeWorldReceiver, and it exists in another file as a blackbox... use interface | |
interface IByeWorld { | |
function value() external view returns (uint); | |
function addValue(uint val) external; | |
} | |
contract CallInterface { | |
uint public val; | |
function example(address _byeWorld) external { | |
IByeWorld(_byeWorld).addValue(123); | |
val = IByeWorld(_byeWorld).value(); | |
} | |
} | |
/* Details & example on 'call' vs 'delegatecall': | |
Let: A -> B -> C | |
A calls B, sends 100 wei | |
B 'call' C, sends 50 wei | |
msg.sender = B, msg.value = 50 | |
execute code on C's state vars, use ETH in C -- B operates on TARGET C | |
A calls B, sends 100 wei | |
B 'delegatecall' C | |
msg.sender = A, msg.value = 100 | |
execute code on B's state vars, use ETH in B -- B operates on SELF B | |
*/ | |
contract CallByeWorldReceiver { | |
function callBWSetandReceive(address _contractAddr) external payable { // this is how to do a low-level call | |
(bool success, bytes memory data) = _contractAddr.call{value: 111, gas: 50000}( // can specify how much gas (careful if it's enough) | |
abi.encodeWithSignature("setXandReceiveEther(uint256)", 456) // note the function prototype format (no spaces) + arguments | |
); | |
require(success, "call setXandReceiveEther failed"); | |
} | |
function callDoesNotExist(address _contractAddr) external { // this will fail and cause error for a contract w/o fallback() | |
(bool success, ) = _contractAddr.call(abi.encodeWithSignature("doesNotExist()")); | |
require(success, "call doesNotExist failed"); | |
} | |
} | |
contract DelegateCallByeWorldReceiver { | |
//uint public someOtherVar; <-- placing another var's declaration here = BAD | |
uint public x; // because of storage layout, the declaration order here must match that of ByeWorldReceiver | |
uint public value; | |
//uint public x; <-- placing/moving x's declaration here = BAD | |
uint public someOtherVar; // appending more variables is fine = OK | |
function delegateCallBWSetandReceive(address _contractAddr, uint _x) external payable { | |
(bool success, bytes memory data) = _contractAddr.delegatecall( | |
abi.encodeWithSelector(ByeWorldReceiver.setXandReceiveEther.selector, _x) // alternative format to encodeWithSignature | |
); | |
require(success, "delegatecall failed"); | |
} | |
} | |
library Math { // fns should not be external or private, if declared public, library would have to be deployed separate rather than inline | |
function max(uint x, uint y) internal pure returns (uint) { | |
return x >= y ? x : y; | |
} | |
} | |
library Array { | |
function findMax(uint[] storage arr) internal view returns (uint) { | |
uint _max = type(uint).min; | |
for (uint i = 0; i < arr.length; i++){ | |
_max = Math.max(_max, arr[i]); // call libraries like APIs in other languages | |
} | |
return _max; | |
} | |
} | |
contract HelloWorldAgain { | |
HelloWorldSender[] public worlds; | |
function createAnotherWorld() external payable { | |
HelloWorldSender world = new HelloWorldSender{value: 111}(); // new used to create contract instances | |
worlds.push(world); | |
} | |
using Array for uint[]; // this attaches the library fns to this data type | |
uint[] arr = [3, 2, 1]; | |
uint getMax = arr.findMax(); // so that we can call it with the access operator directly | |
function hash(string memory text, uint num, address addr) external pure returns (bytes32) { | |
return keccak256( abi.encodePacked(text, num, addr) ); // solidity|eth uses the keccak hash algorithm natively | |
} | |
/* difference between encodePacked and abi.encode() is the latter is prone to collision when the output is the same, ex.: | |
encode("AA","AB") == encode("AAA","B) but encodePacked("AA","AB") != encodePacked("AAA","B) | |
*/ | |
function verifySignature(address _signer, string memory _message, bytes memory _sig) external pure returns (bool){ | |
bytes32 messageHash = keccak256(abi.encodePacked(_message)); | |
// sign(hash(message), private key) | offchain | |
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); | |
// ecrecovery(hash(message), signature) == signer | |
(bytes32 r, bytes32 s, uint8 v) = _split(_sig); | |
return ecrecover(ethSignedMessageHash, v, r, s) == _signer; | |
} | |
function _split(bytes memory _sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { | |
require(_sig.length == 65, "Invalid signature length."); | |
// _sig is a dynamic type, so the first 32 bytes stores the length of the data | |
assembly { | |
r := mload(add(_sig, 32)) | |
s := mload(add(_sig, 64)) | |
v := byte(0, mload(add(_sig, 96))) | |
} | |
} | |
constructor() payable {} | |
// Deletes this contract and force send Ether to *any* address (even if non-payable). | |
function kill() external { | |
selfdestruct(payable(msg.sender)); | |
} | |
} | |
contract ByeWorldAgain { | |
// Call this with HelloWorldAgain's contract address to kill it and force transfer its Ether to this contract. | |
function kill(HelloWorldAgain _kill) external { | |
_kill.kill(); | |
} | |
function getFunctionSelector(string calldata _func) external pure returns (bytes4) { | |
/* This is the first 4 bytes of 'msg.data'. For ex, the function selector of this function | |
"getFunctionSelector(string)" is 0xde38c3d0. | |
*/ | |
return bytes4(keccak256(bytes(_func))); | |
} | |
event Deploy(address addr); | |
// This emits the address of the newly deployed contract. | |
function deploy(uint _salt) external { | |
HelloWorldAgain _contract = new HelloWorldAgain { | |
salt: bytes32(_salt) // random number of choice | |
}(); | |
emit Deploy(address(_contract)); | |
} | |
// To see the old way of deploying using assembly, or how to compute the address before deploying: | |
// https://solidity-by-example.org/app/create2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Made in thanks to Smart Contract Programmer for his educational videos channel. Check out his 0.8 series for a more comprehensive dive as well as using Remix IDE.