Skip to content

Instantly share code, notes, and snippets.

@cwli24
Last active April 29, 2022 23:17
Show Gist options
  • Save cwli24/354120a627087ffab029ea50ee09753e to your computer and use it in GitHub Desktop.
Save cwli24/354120a627087ffab029ea50ee09753e to your computer and use it in GitHub Desktop.
Quick reference to Solidity syntax (vers. 0.8)
// 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
}
@cwli24
Copy link
Author

cwli24 commented Apr 18, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment