General Notes : The doc explaining the syntax part along with some extra explanations that help understand the concept. Based on Solidity Docs, ETH StackExchange and more.
Types :
uint8, uint16, uint<X>are unsigned ints with X bits of capacity.int8, int32, int<X>are signed integers with X bits of capacity.bools are true/false. Default is false.addresstype is a valid address. Comes in 2 flavors, normaladdressandaddress payable. Difference is that payable type has 2 extra methods.transferandsend.Structis also a type. But note that it is just a template. We need to declare somewhere else potentially inside a mapping or something to instantiate the actual variable like, let there bestruct Stemplate and we need to do something likeS a;to create an object a of type S.- Note that strings and bytesX types are Big endian. While other value types are little endian.
bytesX[k]giveskth byte, it is read only. - Mapping can have any type as key except user defined types like Array, Struct and Mapping. The value can be anything, even user defined types
- Mappings can only be defined in storage, i.e as state variables. Even if something like a struct contains a mapping, that struct can’t be instantiated inside a function as memory does not allow mappings even if they are inside some allowed type.
- Enums are just like C. Can have a value of only one of it’s members at a time. Members are indexed from 0. Returning enums returns uint as ABI does not have the concept of Enum. Also, defualt value of enum is first member. Can assign integer
itoenumwho’s value becomesith member. Out of bounds assignment raises apanicerror. Max 256 members.
contract Enum {
// Enum representing shipping status
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}
Status public status;
// Returns uint
// Pending - 0
// Shipped - 1
// Accepted - 2
// Rejected - 3
// Canceled - 4
// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity.
function getChoice() public view returns (status) {
return status;
}
// Update status by passing uint into input
function set(Status _status) public {
status = _status;
}
}
- Struct is a user defined type. Similar to enums, structs can also be imported. Structs are similar to classes. To instantiate a new object, use the constructor. Bear in mind that all new objects are created in memory(as only place we can do this is inside a function) and if the struct were to contain mapping, solidity throws an error.
- Arguments to constructor must follow parameter order or specify
{key:value}object if order isn’t followed. - Array is similar to other langs.
uint[] myArris the way to declare dynamic arrays.uint[10] tenArrayis fixed array. Note that static array values are initialized to zero . Note that functions can also return arrays.return staticOrDynamicArrayis the way. Note that if arrays grow too big, return takes more gas. bytes.concat(...) returns (bytes memory)can be used toconcatvariable number ofbytesand alsobytesXtypes into a singlebytes.- Note that memory can have dynamic arrays via
newbut can’t use resize methods like push and pop on memory arrays. - using
delete myArray[i]does not shrink the array.deletejust changes values to default value. - You can compare two dynamic length
bytesorstringby usingkeccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) - string does not have
lengthproperty to access it’s length. So to make it usable in code that relies onlength, cast it tobyteswithbytes(string)and then use it. - Functions pointers are also a supported type.
function takeUint(uint) externalis also a valid type. It can be assigned and also equality checked with other function pointers.
Variable Scopes :
There are 3 types of variable scopes.
local, declared and used inside functions. Destroyed after execution. Not stored on blockchainstate, declared in the contract scope, stored on blockchain. Accessible by contract functionsglobal, accessed by all. Likemsg.senderandblock.timestamp.
Immutability :
Constant. Declared once and can’t be changedImmutable. Can be changed only on contract instantiation i.e during inside of constructor.
delete operator :
deleteoperator assigns default value to the operand. It does not remove the element.- Using it like
delete arr[3]assigns default value to 3rd element. Array size is not reduced. deleteing static arrays sets all values to zero.deleteing dynamic arrays sets the length of array to zero.mappings can’t be deleted as EVM just storesvaluesatkeccak256(key). And EVM does not know whatkeys are beforehand.deleteingstructresets members to their default values.
Truncation and padding :
- Note that because
bytesandstringare Big endian, any paddings are added after and any truncations are done from first value. - A simple logic is that, paddings are obviously added after the value and truncations are done by fitting values from start and leaving that don’t fit.
bytes2 a = 0x1234;
uint32 b = uint16(a); // b will be 0x00001234
uint32 c = uint32(bytes4(a)); // c will be 0x12340000
uint8 d = uint8(uint16(a)); // d will be 0x34
uint8 e = uint8(bytes1(a)); // e will be 0x12
- Other types are little endian and it is reverse. See above
Accounts :
- Contract addresses are derived from deploying user and their nonce.
- Both are treated by EVM as same way called Accounts. Every account has a persistent key value mapping store. It is called Storage and maps 256-bit words to 256-bit words.
Transactions :
- A transaction is a message from one account to other. Contains payload and ether. The payload is in binary hence the
Abi encodingand if the target address has code, it is executed based on the payload. - A transaction to
nullor empty address creates a contract with payload as bytecode. - Out of gas transaction is reverted and all state changes are discarded
Storage, Memory and Stack :
-
Storage as above stated is a mapped key store data. Enumerating storage is not possible.
-
Costly to modify and even more costly to initialise. Once allocated, storage can’t be allocated again, not possible through any function call.
-
Memory is a freshly created space for every message call. Byte level addressable. Reads are limited to 32 bytes at once. Writes can be 1 byte or 32 bytes, (note that this isn’t storage and the extra gas for reduced size elements theory(from gas optimisations) doesn’t apply here).
-
Any reference or attempt to previously unused memory slot makes the EVM Expand it by a word i.e 32 byte/256 bits. The expansion cost must be paid in gas and costs scale quadratically. Note that practically memory is unlimited. But gas is too much as it increases.
-
EVM is a Stack based machine. Stack limit is 1024 items with each item being 32 Bytes. Note that although limited, stack is the cheapest in terms of gas.
-
Only the topmost 16 Elements can be accessed at most. That too is only in the case of COPY (DUP16) or SWAP (SWAP16). All other operations take the top 2 only.
-
Locations :
-
Function arguments are always stored in memory
-
State variables are always stored in storage
-
Local variables (declared inside function) :
-
If primitves(value types), **stored on **Stack
-
If reference types(arrays, structs) they default to storage, can be explicitly declared as memory too. Note that they are simply references, so until they are assigned they have no memory/actual storage. Once assigned, behaviour depends. Look below!
Now for an investigation into the deep pointer trap!
Have a look at the following code
contract FirstSurprise { struct Camper { bool isHappy; } mapping(uint => Camper) public campers; function setHappy(uint index) public { campers[index].isHappy = true; } function surpriseOne(uint index) public { Camper c = campers[index]; // Look over here, Line 1 c.isHappy = false; // Line 2 } }-
Now recall that our theory says storage can’t be created inside function calls. So what is
Camper cinLine 1? Our theory so far says they default to storage? - Well here’s the deal.
Campers cis actually a Storage Pointer. While actualcvariable is stored inside memory, being a storage pointer it points to storage! - So the change in
Line 2changed actual storage as it is a reference! - Note that nothing is copied here. Storage to Memory copy didn’t happen.
Now take a look at another similar code snippet,
contract AnotherSurprise { struct Camper { bool isHappy; } uint public x = 100; mapping(uint => Camper) public campers; function setHappy(uint index) public { campers[index].isHappy = true; } function surpriseTwo() public { Camper storage c; // Line 1, explicitly set to storage c.isHappy = false; // Line 2 }- Previously we didn’t explicity say
storage. That raises a compiler warning ofVariable is declared as storage pointer. Use explicit “storage” keyword to silence this warning. - Now we did. Now a new warning pops out.
Uninitialized storage pointer. - And surprisingly, Line 2 makes x value equal to 0! Why?
- Becuase we now explicitly declared as storage, The pointer by default points to location 0x00 or first slot. And guess who is chilling out at storage slot 0x00?
- That’s X. And implicitly what’s happening in
Line 2is, we are setting value of X to false which is zero.
So a wise use case for storage pointers is to get read only access if we aren’t careful. Can also use for writing provided we know what we’re doing with manipulating the state reference.
Note : This behaviour is non existent in recent compiler versions. But still have to be careful when using uninitialised storage pointers
-
Calls :
- Errors bubble up. If the inner contract we called via message call fails, our contract will throw a manual error. Only the gas passed to failed contract is wasted.
- The inner contract or the callee in any context will get a fresh instance of memory and the payload in the call referred to as calldata.
- To access the payload of that call, calldata must be referred. Any return variables will be stored at a location of caller’s memory preallocated by the caller.
- A minor gotcha, when forwarding gas to a message call, max you can send is 63/64 of your total gas which maxes out to stack depth of little less than 1000.
- Delegate call is a special call in solidity where code is dynamically loaded from other contract and executed in the state context of calling contract. This makes the library feature possible.
- In create calls that help contracts create other contracts, compared to craeting from an EOA the only diff is that the address of new contract is available on top of stack of creator contract.
Self Destruct :
- Contract can call
selfdestructon itself and then the code and state is removed from the blockchain. - Note that using
selfdestructis not a totally deleting the contract. I think that nodes syncing from ground up can stop before the blockselfdestructwas called and see it’s state and code. (Think? Need some more info) - Even if contract’s code doesn’t have
selfdestructfunction, it can still be done via thedelegatecallorcallcode. (Note thatcallcodewas an older depreciated version ofdelegatecallwheremsg.senderwas the latest caller,callcodecaller instead ofcallcodecaller’s caller i.e theuser).
- Solidity does not support default exports like JS.
- Importing in this way,
import "filename"is a bad choice as namespace will be polluted. Can be used with standard third party files like OpenZeppelin/Uniswap import "filename" as symbolNamelets us access it’s contents likesymbolName.variablewhich is better.
Storage vs Memory vs Calldata :
- Storage is persistent till network lives. Memory is persistent till function call completes. Calldata is persistent till network lives, but is not modifyable and is not tied to the contract. More on that later..
- Storage is the actual data taking space when ETH client is downloaded. So it is a disk write. So it costs more gas
- Memory is data during execution. It can only be created inside functions as local variables. Miner’s RAM/Stack is where memory data lives. After each function call completes it is wiped. Costs less gas than storage
- Calldata is the input data generated by the user and sent as a transaction. All the parameters the user intended to give the contract are located in the calldata. It’s the data field in a transaction.
Why does it matter to the contract/EVM?
- It gives the flexibility to choose where we store data. Memory is cheaper and can be used to read and write intermediate computations.
- Functions can also be typed and can be told to find which data from where
Copies and References :
- Note that calldata can only be read. It is part of a signed ethereum transaction, you can’t change it. So
uint calldata imaginaryNum = 6does not make sense. - Assigning between storage and memory creates copies.
- Assigning between memory to memory or storage to storage creates References! Be careful with the reference traps. Refer to above code for traps
Here comes the stack :
- EVM is a stack based machine. Sure, permanent storage and memory exist but it can’t just be working with them alone. Computers are register based machines, they store intermediate values in these registers and use them to compute further.
- EVM instead of registers uses stack.
- How does it matter to contract? Note that value types like uint, bool when declared inside a function are created on the stack!
- Similar to memory, stack is newly created when an external call is made. Hence the word stacktrace.
- Security Note : EVM allows an opcode that let’s you swap last 16th value. If a function invokes other function, the stack might propagate and the inner function may access the caller’s variables. Turns out, No! Stack is available only between external calls.
Functions :
- Functions in solidity can take in and also return multiple values.
- Destructuring ` (a, b, c) = returnsThree()` is also present in solidity. But note that brackets aren’t squared like in JS
- Functions cannot use maps for inputs or outputs
- If a function returns something, needs to be specified in it’s signature. like
function myFunc returns (uint). For functions returning nothing, don’t specify thereturnskeyword and type, something likereturns (void)isn’t valid in solidity. returnstatement can be omitted if name is specified after returns keyword. Below code is perfectly returns though there’s noreturnstatement.
function assigned()
public
pure
returns (
uint x,
bool b,
uint y
)
{
x = 1;
b = true;
y = 2;
}
-
Viewfunctions mean that they only read. No state change.Purefunctions mean they neither read nor write from state. Note that the constraints apply only on state. You can still create memory objects and interact with the user’s calldata in a normal way. -
Functions are also a valid
value type. They can be equality checked as can be passed as parameters to other functions.contract Oracle { struct Request { bytes data; function(uint) external callback; } Request[] private requests; function storeRequest(bytes memory data, function(uint) external callback) public { requests.push(Request(data, callback)); } function triggerRequest(uint index) external { bytes memory dataToCall = requests[index].data; requests[index].callback(dataToCall); // The function stored during storeRequest is called } } -
External function types are stored as a 24 byte value where the first 20 bytes are the address to invoke the function on and the next 4 bytes are the **function selector ** of the function. These are accesible on the type, i.e
callback.addressandcallback.selectorinside contracts. -
Internal function types can only be passed to internal functions since they are just an internal
JUMP. -
Functions can also be attached to types using the
using functionName for Typesyntax wherefunctionNameis a function at the file level. Theglobalextension can also be used for user defined types where they are defined, to extend the effect globally i.e inherited or imported. Library functions can also be attached asusing {LibraryName.functionName} for someType. -
// File restrictednumber.sol type RestrictedNumber is int256; using {plusOne, minusOne} for RestrictedNumber global; // This extends the effect to places where RestrictedNumber is used function plusOne(RestrictedNumber x) pure returns (RestrictedNumber) { unchecked { return RestrictedNumber.wrap(RestrictedNumber.unwrap(x) + 1); } } function minusOne(RestrictedNumber x) pure returns (RestrictedNumber) { unchecked { return RestrictedNumber.wrap(RestrictedNumber.unwrap(x) - 1); } } /// This is a creation function that ensures that /// values are small enough. The idea is that the function /// RestrictedNumber.wrap should only be used in the file /// that defines the type, so that we have control over /// the invariants. function createRestrictedNumber(int256 value) pure returns (RestrictedNumber) { // Ensure that the number is "small". // Larger constants like 2**200 would also work. require(value <= 100 && -value <= 100); return RestrictedNumber.wrap(value); // Wrap creates the user defined type from underlying } -
// File owned.sol import {RestrictedNumber} from "./restrictedNumber.sol"; contract Owned { RestrictedNumber public ownerCount; mapping(address => bool) public isOwner; constructor() { _addOwner(msg.sender); } function addOwner(address owner) external { require(isOwner[msg.sender]); _addOwner(owner); } function removeOwner(address owner) external { require(isOwner[msg.sender]); require(isOwner[owner]); // Because of <global>, we do not have to add // <using for> in the contract to use the // <minusOne> function. ownerCount = ownerCount.minusOne(); isOwner[owner] = false; } function _addOwner(address owner) internal { require(!isOwner[owner]); ownerCount = ownerCount.plusOne(); isOwner[owner] = true; } }
Visibility :
Public: Can be called internally(from inside current contract) and also externally(other contracts or EOAs)private: Can only be called from inside of a the contract internally.internal: Only internally from inside a contract or by children inheriting it. Something on the lines of Java’s protected.external: Can only be called externally. To call from inside the contract, usethis.f()
About return values :
- Calling functions as an EOA/User :
- Only constant i.e view or pure functions can return values to users.
- Non constant i.e state changing functions return a TXN HASH and cost gas
- The only way to extract any values from state changing function to user/UI is Listening to events.
- Calling functions of a contract from another contract :
- Both constant and non constant functions can return values when the caller is a contract.
- Note that constant functions too cost a little when called from a contract .
- There’s always a cost incurred when calling a smart contract. Constant or Non Constant. But, becuase the nodes you use or third party APIs run actual nodes, they return the data and don’t make an EVM call and hence no gas.
- Because the contract running or the EVM for that matter doesn’t have the concept of API nodes, it always bills the caller for any function. Hence, It costs gas to call constant functions of a contract from other contract.
require, revert and assert:
- There are two kinds of Errors that EVM can throw.
ErrorandPanic. - Consider
Errorlike a soft check exception you can throw in your contracts. Like Input validation or return values or state checks Panicis the actual compiler/EVM screaming that something terribly bad has happened. Likes of them are divide by zero, overflows, negative indices, large memory allocation. There’s also a special one called compiler inserted panic. More on that later.Erroris the exception thrown by your code. Your code intended to throw that error and is not some fatal error likepanicthrown by EVM.- Also note that
assertconsumes all gas and throws whilerequirerefunds left over gas and throws. - So consider
requireas your intended check whileassertis a sanity check. Failingassertshould mean an overlooked edge case or a bug in code while failingrequiremeans just not being eligible.
Ways you can throw Error :
- Now,
requiresyntax isrequire(checkReturnsTrueOrFalse, Error). ThecheckReturnsTrueOrFalseshould evaluate to abool. - If it evaluates to a
false, an Error is thrown withErroras description. - Syntax for revert is
revert(descriptionString)or justrevert(). This also throws an error.
When to use require vs revert :
-
requirecan only perform small checks and then throw error. -
If the error checking code is complex then use
revertlike this,if(complexErrorCheck == false){ revert("Error Description"); } -
Note that
requireonly creates and throwsErrorof typeError("Description String"). Custom Error objects can’t be used, use revert for that -
revertcan throw custom errors like this,
if(complexErrorCheck == false){
revert customError();
}
Ways a Panic can occur :
- Remeber compiler inserted panic from above, let’s talk about that.
- Along with examples like above, panic can occur in two extra compiler/user invoked ways
Panic 0x00is thrown by compiler inserted panicPanic 0x01is thrown by Failing an assert condition!
Assert :
- Assert syntax is
assert(onFalseThrowPanic). If the check becomes false, a panicPanic 0x01is thrown.
Modifiers :
- Function modifiers are special code that can be injected before a function on which the modifier is applied is called,.
- They can be used for things like Access control, some input checks and most importantly Re-Entrancy Guards.
- Actually, modifiers are invoked before executing the function. But, the modifier can choose Where to pass the power back to it’s function. The
_;is used to execute the actual function. Check below
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_; // Look over here. Notice the _;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within `msg.sender.call` cannot call `f` again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
- In the above code when
ffunction is called, firstnoReentracymodifier is executed. While execution, the modifier performs it’s logic and then calls it’s function using_;. The_;is just a placeholder for modifier to pass the power to it’s function. Note that additional logic can also be performed after the function executes. - The modifier above also does some state changes after function has executed.
- So modifiers seem to be a wrapper function around actual functions. Hence also note that arguments to function are also forwarded to the modifiers.
- But arguments need to be passed to modifiers in the function signature like
function someFunction(address _user) notSpecificAddress("0x......"), here the value is hardcoded but it can be a state variable or an argument passed down by the function. - Note that the placement of
_;is key to writing secure code. So for starters, include it at end of custom modifers.
Events :
- Events are inheritable members of contracts. The syntax is
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
- And to emit them,
emit Deposit(msg.sender, _id, msg.value);
- Any web3 JS library can then subscibe to these events.
- You can add the attribute
indexedto up to three parameters which adds them to a special data structure calledtopicswhich can be used to create filters in webJS libraries to listen to events. - Parameters without
indexedatrtribute are ABI Encoded and stored in logs. - Note that the hash of the event is already one of topics. So if you declared the event as
anonymous, then a field is left free and you can have total four indexed parameters. - But the catch is that anonymous events can’t be filtered easily, you need to know the contract address to listen to anonymous events.
Purpose of Events?
- Note that only constant i.e view or pure functions can return values.
- So how do you update your Dapp UI if write/state changing functions are called? That’s where events step in!
- Also check ethers docs to see how to call constant and non-constant functions.
Constructors :
-
Constructors of a contract run before deployment of the contract. The contract deployment cost rises linearly with size of the code. But note that constructor code and internal functions code used only in constructor is not billed in gas.
-
State variables are initialized to default values even before running the constructor.
-
A contract needs to implement all it’s parent contract’s constructor compulsorily. Else it is declared abstract.
-
2 ways to do that,
-
Specify in the inheritance list itself. Better suited when args are constants
-
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Base { uint x; constructor(uint _x) { x = _x; } } // Either directly specify in the inheritance list... contract Derived1 is Base(7) { constructor() {} } -
Specify in the modifiers of constructor. Used when args are actually args to child constructors
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Base { uint x; constructor(uint _x) { x = _x; } } contract Derived2 is Base { constructor(uint _y) Base(_y * _y) {} }
-
-
Inheritance is linearized see C3 Linearization below . Which means always specify the inheritance directive
contract child is grandParent, Parentlike that. Doing something likecontract child is Parent, grandParentdoes not work. Simply put, **List highest parents first in inheritance tree. TOP to BOTTOM becomes **contract child is Top1, Top2, Top3, oneLevelAboveChild. -
Note Parent constructors are run not in modifier specified way. They always run in the same linearized manner. See below
contract Base1 { constructor() {} } contract Base2 { constructor() {} } // Constructors are executed in the following order: // 1 - Base1 // 2 - Base2 // 3 - Derived1 contract Derived1 is Base1, Base2 { constructor() Base1() Base2() {} } // Constructors are executed in the following order: // 1 - Base2 // 2 - Base1 // 3 - Derived2 contract Derived2 is Base2, Base1 { constructor() Base2() Base1() {} // Modifier order does not // matter. Follows linearised //order } // Constructors are still executed in the following order: // 1 - Base2 // 2 - Base1 // 3 - Derived3 contract Derived3 is Base2, Base1 { constructor() Base1() Base2() {} // Same here } -
Becuase
Base1andBase2don’t have any inheritance relation, the linearization order is the order we have given in theis <>directive **. Hence constructors also run in that order i.e **linearised order. Had they been related in a inheritance relation, we need to make sure outis <>directive is Most Base like to most child like and constructors run in that order.
C3 Linearization :
- Very important check this. Note that as stated above, parent constructors are called in order of left to right of
isdirective. The below discussion is how super changes based on linearisation.
contract Granny {
event Say(string, string);
constructor() public {
emit Say('I am Granny and I am cool!', 'no one');
}
function getName() public returns (string memory) {
return 'Granny';
}
}
contract Mommy is Granny {
constructor() public {
emit Say('I am Mommy and I call ', super.getName());
}
function getName() public returns (string memory) {
return 'Mommy';
}
}
contract Daddy is Granny {
constructor() public {
emit Say('I am Daddy and I call ', super.getName());
}
function getName() public returns (string memory) {
return 'Daddy';
}
}
contract Kiddo is Mommy, Daddy {
constructor() public {
emit Say('I am Kiddo and I call ', super.getName());
}
}
The output becomes :
I am Granny and I am cool
I am Mommy and I call Granny
I am Daddy and I call Mommy // What in the world happened here?
I am Kiddo and I call Daddy
Magic? No, it’s C3 Linearization :
-
Solidity uses C3 linearization to resolve the diamond inheritance problem. It serializes the diamond shape into a linear line. The order of inheritance is still conserved, but sibling parents are placed next to each other, based on who was called first.

-
Imagine how the line would form. Initially no lines are there. Lines are finalized only when all parent constructors are called and child constructor is invoked.
-
First
ChildcallsMommy. Still no lines. -
Mommythen callsGrannyandGrannyhas no parents to line up above. Call is back toMommyconstructor. -
Mommyconstructor is called and a line is formed to where call came from recently. Thesuperor parent ofMommyis granny. -
Then based on
Child’sisdirective,Daddyis called.Daddytries to callGrannybut remember that virtual inheritance prevents this. Already an object is existing in the inheritance graph, so no new Grannys. -
Now something interesting happens. Because recent constructor was called from
Mommy, the parent ofDaddybecomesMommy. -
Finally,
childconstructor finishes up andDaddybecomes the parent as it is the recently called one.
So, When a child function calls super.foo(), solidity searches for it in parents one level above in the order of C3 Linearisation.
Inheritance :
- If a contract inherits from multiple contracts and both those contracts inherit from same base contract (google diamond pattern), then only one copy of top most contract is created. See below. Similar to Virtual Inheritance from C++.
contract Owned {
string name = "John"
}
contract Destructible is Owned {
// string name = "Something"; not allowed as it shadows
constructor(){
name = "Joseph" // This directly points to parent's name
}
}
contract Named is Owned, Destructible { // Inherits only a single 'Owned', not twice.
}
A case analysis :
-
A contract has defined some functions as
virtual. Another contract inherits from it.- This is the case with contracts like
ERC20. They define some functionality and let children extend that functionality. Note that the extension is a Choice. - For example, the
transferfunction ofERC20is virtual. Which means you can implement your owntransferfunction using override, while also using openzeppelin’s code by just sayingsuper.transfer().
- This is the case with contracts like
-
A contract has declared some functions as virtual but did not define them.
- This makes the above contract
abstract. Any child inheriting them can’t be instantiated without defining that functionality. - Contrast to above, this isn’t a choice, it’s a burden on the child contract.
- But why? Consider a case where there is an
interface(more on interface later) orabstract contractcalledwillDoSomething. - This tells the compiler/guarantees the user that contracts inheriting
willDoSomethingwill actually do something. - This can be used to make sure that A standard is adhered and implemented. Hence the word
IERC20in libraries.IERC20is the interface telling us the rules of the ERC20 standard. The interface itselfIERC20does not ** implement anything. It **Forces inheriting members to complete the functionality so that standards are met. - A little gotcha,
abstract contracts cannot cannot override an implemented virtual function with an unimplemented one. In simple words, you can enforce your children to write/define an implementation, but can’t deny them the functionality passed from above.
- This makes the above contract
-
An
abstract contractorinterfaceis defined/imported and not inherited. Used as a variable inside a contract- This is the case when you want to assure compiler that it need not throw undefined errors.
- Consider a case where your contract wanted to use Uniswap contracts inside your contract. How do you tell your compiler that Uniswap docs guarantee this functionality, so I’m gonna use it?
- This is where interface comes in. See below :
import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol'; contract swapExample { ISwapRouter public immutable swapRouter; constructor(ISwapRouter _swapRouter) { swapRouter = _swapRouter; } function swapExactInputSingle(uint256 amountIn) external returns (uint256 amountOut) { ISwapRouter.ExactInputSingleParams memory params = // Using interface's struct ISwapRouter.ExactInputSingleParams({ tokenIn: DAI, tokenOut: WETH9, fee: poolFee, recipient: msg.sender, deadline: block.timestamp, amountIn: amountIn, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }); // The call to `exactInputSingle` executes the swap. amountOut = swapRouter.exactInputSingle(params); // Using interface's function } }- Uniswap has developed and deployed their SwapRouter contract somewhere else but your compiler needs to have a reference to check what functions and structs/vars it supports or not.
- For that purpose, uniswap has released an Interface so that you can import it and make the compiler not yell at you while also getting typed reference.
Interface vs abstract contract :
- Both are used to force reference implementation of children as told above.
- Differences :
abstract contractis still a contract. It can **have state variables, inherit from other contracts, have a contructor and even define some modifiers ** . All that a normal contract can do, just instantiation can’t be done.interfacecannot do all that.interfacepurely is a template forcing inheriting children to implement it. So just a template, nothing like a contract. So it cannot have state, cannot define ANY function implemented, cannot have a contructor, cannot declare modifiers and all functions must be virtual and external although virtual keyword is implicit.- But the template extenstion is still possible.
abstract contracts can inherit whatever they want andinterfacescan only inherit other interfaces.
Function Overriding :
- Functions in base contracts need to specify that they can be overriden by child functions using the
virtualmodifier. -
Functions in child contracts need to specify which functions they are overriding by using the same signature as base contract function. I.e same name and return type, and also need to specify the
overridemodifier. - You can also always explicitly call a function of a contract in the inheritance path by specifying which contract you are referring to
twoLevelsUpParent.foo(). Overrideneeds to specify which contract’s functions you are overriding when multiple inheritance is in play. See below
contract A {
uint public x;
function setValue(uint _x) public virtual {
x = _x;
}
}
contract B {
uint public y;
function setValue(uint _y) public virtual {
y = _y;
}
}
contract C is A, B {
function setValue(uint _x) public override(A,B) {
A.setValue(_x);
}
}
- Functions can be both virtual and override at the same time. So they can implement/extend some functionality via
overrideand also force children to extend this functionality throughvirtual. - Note that you can’t override parent’s
externalfunction and then try something likesuper.this.f(). Simply put, you can’t call parent’s external functions if you’ve already overriden them in the child.
Payable :
address payablecan be sent ether from contracts. Normaladdresscan be type casted intoaddress payableusingpayable(address).- For a function to receive ether, include a modifier called
payable. Sending ether to a function withoutpayablethrows an error. - Constructors can also be
payable.
Receiving Ether :
-
A contract can receive Ether in 3 total ways based on functions it implemented and the call used to call the functions.
-
The call’s
msg.datais non empty. Which means the call is trying to call a function-
If that function is
payablethen it’s all fine. ether balance of contract increased and it’s code is executed -
If that function is
non payable, transaction is rejected -
If the function does not exist, a fallback function is called if present. There can be atmost one fallback function. The exact syntax is
fallback () external [payable] {...} // OR fallback (bytes calldata _input) external [payable] returns (bytes memory _output) {...} -
Notice that again no function keyword.
-
-
The call’s
msg.datais empty. Which means it is a simple ether transfer usingsendortransfer.-
First the
receivefunction is called if it exists. The exact syntax isreceive() external payable { ... }Note that
receivecannot have any arguments and cannot return anything. It can do state changes and some operations but no inputs or outputs. Contrast this withfallbackwhich can have inputs and outputs. -
If the
receivefunction does not exist, again afallbackfunction is called.
-
A Note :
Transactions without
msg.datathat send ether to a contract fail and get rejected if the contract does not have areceiveand also does not havefallback. They also fail if themsg.dataintends to call a function but it is notpayable.- A special case, due to design of EVM, coinbase(miner reward) txns and self-destruct txns can forcibly send ether to a contract. It doesn’t matter receive or payable exists or msg.data is empty or not. The Ether is FORCED IN.
-
Sending Ether :
You can send ether with 3 methods :
address.transfer(etherAmount): Tries to sendetherAmountto an address. If balance of contract is less than etherAmount or runs out of gas, the transactions is reverted and exception is thrown. Sends only 2300 gas.address.send(etherAmount): Tries to sendetherAmountto an address. If fails, returns false, does NOT throw an exception. So always check the return values here. Sends only 2300 gas.address.call{gas : gasForwarded, value : etherAmount}(payload): The payload is an encoded string, trying to call a contract. Returns true if successful else false. Note that call also returns the return data of a callee if it’s a contract. Sends specified gas, if not specified all is transferred.
Delegatecall :
- Syntax is
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
// NOTE: Deploy this contract first
contract B {
// NOTE: storage layout must be the same as contract A
uint public num;
address public sender;
uint public value;
function setVars(uint _num) public payable {
num = _num;
sender = msg.sender;
value = msg.value;
}
}
contract A {
uint public num;
address public sender;
uint public value;
function setVars(address _contract, uint _num) public payable {
// A's storage is set, B is not modified.
(bool success, bytes memory data) = _contract.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
}
- Executes the code of contract B with the storage of contract A. This functionality is used mostly in upgradeable contracts.
- Retains and forwards the
msg.senderandmsg.value.thisin external contact/library refers to callee’s context. - When an EVM call occurs for external functions(
delegatecall), storage references are not copied asdelegatecallshares the state. Butmemoryobjects are copied and sent asmemoryis not shared indelegatecall.
Libraries :
- They are pre deployed code which can be used without increasing the contract code size.
- If all the functions of library are internal, then the library is included in the contract code. So there’s no
delegatecallif all functions are internal, just aJUMPis used. - Only if the library contains external functions, ** then it needs to be **linked during deployment and comes the
delegatecall. - Also, library functions not modifying state i.e view/pure can be called directly. Makes sense, no need of
delegatecallwhen no state changes. - For example, if we use a library called
MathandHeapwhich have external functions, then we need to link it during compile time with name and deployed address :
solc --libraries "file.sol:Math=0x1234567890123456789012345678901234567890 file.sol:Heap=0xabCD567890123456789012345678901234567890"
Global Units :
weigweiandetherare global keywords in solidity. Values are..
assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);
- Time units are present but should not be used as exact measures.
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
-
block.<property>is also a globally available data. Check exhaustive list. Some are listed belowblock.coinbaseis the miner’s address. It ispayableblock.timestampis the current block’s timestamp since UNIX epochmsg.senderis the recent caller.tx.originis the originator of the whole call chain.msg.valueis theethervalue of transaction.gasleft()returns gas left in the current call.
-
Some considerations : properties like
tx.*andblock.*fail/not accurate when being executed off-chain or not executed in an actual block. Think flashbots simulate part. The above properties might be supplied diff value compared to what original can be in actual mainnet block. -
Do not rely on
block.timestamporblockhashas a source of randomness. -
Use
keccak256(bytes memory) returns (bytes32)can be used to computekeccak256hash of any kind of inputs. Most useful in comparing two strings or bytes. -
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)can be used to verify a signature. Ask the user on Dapp to sign a message, take the input as bytes and then slice it as below:r= first 32 bytes of signatures= second 32 bytes of signaturev= final 1 byte of signature- That will return an
addressof who signed the signature. Note that thehashis the hash of the message user has signed, the format for that is,
hashToBeSuppliedToEcrecover = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n",len(_message), keccak256(_message))); -
There seems to be an issue with signature duping but was fixed, while
ecrecoveris not fixed. So docs recommend using OpenZeppelin ECDSA Library. -
address.codereturns code at the address. Is emptybytesarray if account is anEOA. -
address.balanceis the balance of an account.
Contract Meta :
-
Can use following properties to gert more info on contract types.
-
Let
Cbe a contract type,Ibe an interface type.-
Code Property type(C).nameName of contract type(C).creationCodeThe compile time bytecode type(C).runtimeCodeThe bytecode after constructor ran. There are some caveats, check docs. type(I).interfaceIdA bytes4interface type. Use to check if a contract supports interfaces likeIERC20and so on..type(C).functionNameThe function pointer of a function inside contract C. Can be used as a type and also in external calls using encodeCall
-
ABI Usage :
(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))decodes the abi encoded data.abi.encode(...) returns (bytes memory)encodes stuff using padding and hence no collisions when dynamic data is involved.abi.encodePacked(...) returns (bytes memory)does packed encoding. Should NOT be used when >2 dynamic arguments are involved due to hash collision, likeA, ABandAA, Bgive same encoding here due to no padding and hence their hashes collide.abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)same asabi.encodebut prepends theselector. Useful when doing raw txns, selector is used to specifyfunction signature.abi.encodeCall(functionPointer, ...) returns (bytes memory)is same as above but a function pointer is passed.