Contracts on Ethereum & EVM compatible Blockchains [also helpful for EOSIO Developers]
- The contract code can't be updated (by default), but all the state variables can be updated.
- The contract code can be updated via proxy method using technique proposed by Openzeppelin (Transparent, UUPS), Diamond standard (more robust, modular).
- evm-boilerplate
- formatter YouTube
- Open Settings
- type 'save format' >> tick the "Editor: Format on Save"
- type 'formatter' >> set the "Editor: Default Formatter" to Prettier (by esbenp....)
- type 'solidity formatter' >> set the "Solidity: Formatter" from
none
toprettier
- Now, on saving any solidity (
*.sol
) file, it will automatically format.
- packages:
-
Write Solidity based contracts here.
-
Deploy, test as a user like an IDE using a local file
- Follow this guide
- Install remixd using
npm install -g @remix-project/remixd
globally ornpm install @remix-project/remixd
locally. - And then "Connect to localhost" in the Remix Website.
-
Getting Started with Remix IDE CLI:
-
Just run
remixd -s .
in the integrated terminal opened atsc-sol/
folder. It will open the Remix IDE in the browser. And then one can access the OZ ornode_modules/
folder altogether.- 3 ports opened: 65520, 65522, 65523 generally
[INFO] Sun Jan 15 2023 16:50:41 GMT+0530 (India Standard Time) remixd is listening on 127.0.0.1:65520 [INFO] Sun Jan 15 2023 16:50:41 GMT+0530 (India Standard Time) slither is listening on 127.0.0.1:65523 [INFO] Sun Jan 15 2023 16:50:41 GMT+0530 (India Standard Time) hardhat is listening on 127.0.0.1:65522
-
ctrl+c to shutdown the node.
-
-
Use inside a project dir like this:
$ remixd -s <project_dir> --remix-ide https://remix.ethereum.org
. E.g.$ remixd -s ./contracts --remix-ide https://remix.ethereum.org
or$ remixd -s . --remix-ide https://remix.ethereum.org
or$ remixd -s .
Install remixd
globally using npm: $ npm install -g @remix-project/remixd
Upgrade remixd
globally:
$ npm uninstall -g @remix-project/remixd
$ npm install -g @remix-project/remixd
Usage:
Also, get to see the console.log()
statements in the Remix IDE terminal in the browser.
CALL
[call]from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4to: Greeter.greet()data: 0xcfa...e3217
transact to Greeter.setGreeting pending ...
[vm]from: 0x5B3...eddC4to: Greeter.setGreeting(string) 0xd8b...33fa8value: 0 weidata: 0xa41...00000logs: 0hash: 0x3c3...8b9b2
console.log:
Changing greeting from 'Happy Army Day' to 'Happy Bday'
for a solidity code like this:
contract Greeter {
// state variables
function setGreeting(string memory _greeting) public {
console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
greeting = _greeting;
}
}
Refer this
- Ethereum smart contracts are generally written in Solidity and then compiled into EVM bytecode and ABI via
solc
.
NOTE: most people do not compile contracts directly through commands, because there are very convenient tools and frameworks such as remix or truffle or hardhat.
- Comparo
Solidity | EOSIO | |
---|---|---|
Compile Contract | solcjs --abi --bin hello.sol | eosio-cpp hello.cpp -o hello.wasm |
Deployment contract | hello=(web3.eth.contract([…])).new({…}) | cleos set contract hello ./hello -p hello@active |
Call contract | hello.hi.sendTransaction(…) | cleos push action hello hi '["bob"]' -p bob@active |
- The EVM bytecode is further converted into OPCODE which looks like this:
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH3 0x11 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x40 MLOAD PUSH3 0xB7A CODESIZE SUB DUP1 PUSH3 0xB7A DUP4 CODECOPY DUP2 DUP2 ADD PUSH1 0x40 MSTORE DUP2 ADD SWAP1 PUSH3 0x37 SWAP2 SWAP1 PUSH3 0x1E4 JUMP JUMPDEST CALLER PUSH1 0x0 DUP1 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MUL NOT AND SWAP1 DUP4 PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND MUL OR SWAP1 SSTORE POP DUP2 PUSH1 0x1 SWAP1 DUP1
- The EVM opcode gives instruction to EVM to execute commands.
- Bytecode to Opcode converter tool
NOTE: It's almost impossible to generate Smart contract code from EVM ABI & Bytecode.
The contract compilation can happen using EVM frameworks like Hardhat, Truffle, Foundry (my favourite 2023 onwards), Brownie.
- The address of the contract account (not external account) is automatically generated at deployment time, and the contract can never be changed once deployed. The deployer's address & its nonce are used to form a hash (32-byte).
Hash(RLP_encoding(deployer_address, nonce))
is from where the rightmost 20 bytes are used as the address of the contract. - In EVM chains, gas fee is analogous to EOS's CPU, NET (follow staked or pay-per-use model).
EOSIO's CPU & NET
<->ETH's gas fee
- In EVM chains, the contract data storage is unlimited unlike EOSIO's RAM (follow staked or pay-per-use model).
EOSIO's RAM
<->ETH's unlimited storage
.But, the contract code is limited to 24KB in EVM chains.
- The EVM has a stack machine with max. 1024 items (of each 256-bit (32-byte) word).
This 256-bit word size is chosen to facilitate cryptographic operations, such as hashing and elliptic curve computations.
- In ETH, an object is created based on ABI in the
geth
console and then calls thenew()
method to initiate a contract creation txn (parameters contain bytecode), whereas in EOSIO'scleos
tool,set()
method is used to specify the bytecode and the directory where the ABI is located.In EOSIO, the contract account name is not generated automatically, but it has to be created manually/automatically by the user i.e. RAM, CPU, NET has to be provided. The contract account name is the same as the contract name in terms of rules.
- Remix
- Environments
For seeing the contract data, function details, Remix IDE is the best tool for EVM contracts.
- Environments
JavaScript VM: All the transactions will be executed in a sandbox blockchain in the browser. This means nothing will be persisted when you reload the page. The JsVM is its own blockchain and on each reload it will start a new blockchain, the old one will not be saved.
Injected Provider: Remix will connect to an injected web3 provider. Metamask is an example of a provider that inject web3.
Web3 Provider: Remix will connect to a remote node. You will need to provide the URL to the selected provider: geth, parity or any Ethereum client.
- The call method is only executed locally, does not generate transactions and does not consume gas,
sendTransaction()
method will generate transactions and consume gas, the transaction is executed by the miner and packaged into the block, while modifying the account status.
- Summary: Prefer CamelCase for contracts & firstSecond type of font style for functions, variables.
- Elements
- The pragma statement
- Import statements
- Interfaces
- Libraries
- Contracts
NOTE: Libraries, interfaces, and contracts have their own elements as well. They should go in this order:
- Type declarations
- State variables
- Events
- Functions
Stack:
- Nature: A Last-In-First-Out (LIFO) data structure.
- Size: Limited to 1024 elements, each being 256 bits (32 bytes).
- Purpose: Holds intermediate values during execution, facilitating operations like arithmetic calculations and temporary data handling.
- Persistence: Data exists only during the execution of a single operation; it’s cleared immediately after.
- Access: Directly manipulated using opcodes such as
PUSH
,POP
,DUP
, andSWAP
.
Memory:
- Nature: A linear, byte-addressable, and expandable array.
- Size: Starts at zero and can grow dynamically as needed during execution.
- Purpose: Stores temporary data that may be needed across multiple operations within a single transaction, such as arrays or buffers.
- Persistence: Data persists only for the duration of the transaction; it’s cleared once execution completes.
- Access: Managed using opcodes like
MLOAD
(to read) andMSTORE
(to write).
Storage:
- Nature: A key-value store associated with each contract, where both keys and values are 256-bit words.
- Size: Theoretically unbounded, but practical usage is constrained by gas costs.
- Purpose: Holds persistent state variables that remain consistent across transactions, such as balances or contract settings.
- Persistence: Data remains intact between transactions and is part of the blockchain’s state.
- Access: Interacted with using opcodes like
SLOAD
(to read) andSSTORE
(to write).
Summary:
- Stack & Memory are both volatile in nature.
- Storage is non-volatile in nature.
- Storage cost ranking (least to most):
- Stack
- Memory (costs increasing as more memory is used)
- Storage (due to permanent storage in blockchain).
-
max code size:
24 KB
-
max storage size:
2^261
i.e.3.7 x 10^69 GB
. -
Contracts and libraries should be named using the CapWords style. Examples: SimpleToken, SmartBank, CertificateHashRepository, Player, Congress, Owned.
-
Contract and library names should also match their filenames.
-
If a contract file includes multiple contracts and/or libraries, then the filename should match the core contract.
-
Contracts consist of 2 main types:
- Persistent data kept in state variables
- Runnable functions that can modify state variables
-
Each contract can contain declarations of State Variables, Functions, Function Modifiers, Events, Errors, Struct Types and Enum Types.
-
Furthermore, contracts can inherit from other contracts.
NOTE: unlike in other languages, you don’t need to use the keyword
this
to access state variables.
- Creating contracts programmatically on Ethereum is best done via using the web3 packages:
web3.js
,ethers
(typescript). It has a function calledweb3.eth.Contract
to facilitate contract creation. - A constructor is optional. Only one constructor is allowed, which means overloading is not supported.
- When a contract is created, its constructor (a function declared with the constructor keyword) is executed once. All the values are
immutable
: they can only be set once during deploy. - A constructor is optional. Only one constructor is allowed, which means overloading is not supported.
- Constructor can't be called from inside another function
- Inheritance:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
import "./Owned.sol";
contract Congress is Owned, TokenRecipient {
//...
}
- The contract should be named using the CapWords specification (first letter)capital)
contract BucketCrow {
// ...
}
- After build, a contract looks like this:
where, there is a bytecode.
NOTE: When a transaction is sent to a block all it's actions are considered confirmed i.e. based on which some values can be set like
id
here. Either of the action if fails, the whole transaction is reversed.
Try this example on Remix.
- Each of the action/operation is executed provided all are successful. E.g.
function createAuction(address _asset, uint256 _startsAt, uint256 _endsAt)
external
whenNotPaused
nonReentrant
returns (address auction)
{
...
...
bytes memory bytecode = type(Auction).creationCode;
bytes32 salt = keccak256(abi.encodePacked(_asset, _startsAt, _endsAt));
assembly {
auction := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
// console.log("Auction created: %s", auction);
IAuction(auction).initialize(owner(), msg.sender, _asset, _startsAt, _endsAt);
...
...
// transfer ownership of the auction to the Auction contract created
bool success = IGenericERC20(_asset).transferFromOwnership(auction);
require(success, "transferFromOwnership failed");
}
In the above code snippet, there are 3 actions:
create2
actioninitialize
actiontransferFromOwnership
action
- A constructor is optional. Only one constructor is allowed, which means overloading is not supported.
- A constructor is executed when the contract is created.
- A constructor can't be called from inside another function.
- A constructor can't be called directly.
- A constructor can't be inherited.
- During inheritance of abstract, contract the constructor gets run by default. E.g.:
contract AuctionRepository is Ownable, Pausable, CheckContract {
}
Here, the constructor of Ownable
, Pausable
, CheckContract
will be called, if available.
- The Solidity/Vyper/Fe (or any language) code can be converted into a binary (represented in hexadecimal format) understandable by the machine: EVM i.e. Bytecode
- You can get bytecode from the source code which can then be compiled to bytecode or
.bin
format. - You can download the bytecode (which is normally uploaded by the contract author/owner).
- The EVM bytecode above is nothing more than a sequence of EVM opcodes written in hexadecimals.
- ABI is what creates the interaction link between a client (directly from an EOA or an interface) and a smart contract bytecode (the contract logic in EVM opcodes).
- ABI defines clear specifications of how to encode and decode data and contract calls.
- Therefore in Ethereum and any EVM based chain, the ABI is basically how contracts calls are encoded for the EVM (so that the EVM understands which instructions to run).
ABI = specification for encoding + decoding
- understand function signature
There are some standard (abi.encode
for encoding data), non-standard/packed (abi.encodePacked
) encoding functions associated with abi:
---1---
abi.encode("AbhijitRoy")
> 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a416268696a6974526f7900000000000000000000000000000000000000000000
abi.encode("Abhijit Roy")
> 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b416268696a697420526f79000000000000000000000000000000000000000000
---2---
abi.encodePacked("AbhijitRoy")
> 0x416268696a6974526f79
abi.encodePacked("Abhijit Roy")
> 0x416268696a697420526f79
---3---
abi.encodePacked("Abhijit", "Roy")
> 0x416268696a6974526f79
abi.encodePacked("Abhijit", " Roy")
> 0x416268696a6974526f79
In EOSIO, all the account names are encoded into a 64-bit integer. This is done by taking the first 12 characters of the account name and converting them to a base-32 number. The remaining characters are ignored. This is done to save space on the blockchain.
- In
1
, if space occurs in input, the encoded output is different along with the offset. - In
2
, if space occurs in input, the encoded output is different without the offset. - In
3
, if space occurs in input, the encoded output is same without offset. So, it trims the spaces used for before & aftercomma
.
Here, encodePacked
drops the offset when compared to encode
.
When calling SC, to encode function calls for external contract interaction, there are 2 functions:
abi.encodeWithSignature(...)
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSignature("transfer(address,uint256)"), to, value);
...
}
abi.encodeWithSelector(...)
bytes4 SELECTOR = bytes4(keccak256(bytes("transfer(address,uint256)")));
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
...
}
These are the 2 functions which can be used in Solidity to prepare payloads in raw bytes for external contract calls. Such payloads can be passed as parameters to the low level Solidity .call
✅, .delegateCall
✅, .staticCall
[❌Deprecated].
Both are same functions implemented differently. Easy to use abi.encodeWithSignature
[Personal preference though].
abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)
is equivalent to
abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)
The Solidity variable types are converted into ABI as shown below:
These are the observations:
- In a nutshell, a variable of type
address payable
orcontract
will be encoded/decoded under the hood by the ABI as a standardaddress
. enum
is converted to the lowest uint i.e. 8 bits or 1 byte.uint8
=> max. 256 items can be held by an enum since^0.8.0
Solidity version.
Read more
where, no bytecode.
Read more
We don't need to implement the view
functions for public
state variable. Like in this eg:
Here, usdc
is defined as public
state variable. That means we don't need to define a usdc()
function. But, it can be added to contract's interface in order to access the usdc
state variable inside another contract like this:
This is related to ECDSA. Inside SC, a message can be checked whether it was signed by the required address or not.
Here, the process goes like this Video.
Theory:
- Create a message
- Sign the message with your private key
- Send these to verifier/validator
- original message
- signed message
- signer address (can be public key like in EOSIO chains)
- Now, the validator use
ecrecover
method to check whethersigner address == address obtained from (original_msg, signed_msg)
.
Practical: The steps for usage (in coding) is as follows:
msg = "I am unwell"
messageHash = keccak256(msg);
// a.k.a. signature
signedMessageHash = sign(messageHash, privateKey);
// verify the signature
ecrecover(signedMessageHash, _signature) ==_signer;
To use the actual solidity code, refer this.
We know that every (write) interaction on the Ethereum blockchain requires a small amount of ETH on the caller address.
This sounds really awful from a UX perspective for token holders, as users (holding fiat money) first need to buy (onramp) ETH from a CEX and transfer it to the wallet address (caller). But wait, isn’t it the case — very simplified — that at the most foundational blockchain level, it is simply a matter of verifying the signed payload, i.e. off-chain cryptography?
Ah yes, that sounds right! So how about the wallet user simply signs the payload off-chain and someone else (e.g. an operator) broadcasts and pays for the transaction?
There you go, we have the solution: meta-transactions.
Definition:
A meta-transaction is a regular Ethereum transaction which contains another transaction, the actual transaction. The actual transaction is signed by a user and then sent to an operator or something similar; no gas and blockchain interaction required. The operator takes this signed transaction and submits it to the blockchain paying for the fees himself. The forwarding smart contract ensures there is a valid signature on the actual transaction and then executes it. More
Now, with EVM SC, the transactions can be gasless (meta-transactions) for users, where the contract owner would pay the gas fee.
This feature is accessed via off-chain method through cloud infrastructure setup by Biconomy. So, access via setting up on their website.
Unlike EVM chains, this gasless feature is available on-chain with EOSIO chains where the SC developer can program so that the contract pays the gas (CPU, NET) instead of the caller (user).
Following are the steps: Video
-
Sign up on Biconomy with email address & then verify.
-
Give a native coin value limit like ETH amount (for Ethereum) per DApp, per user, per API key.
- Here, Biconomy has deployed a
Forwarder.sol
SC for paying the gas-fee on behalf of the the contract deployer (who ensures native coin in the Biconomy setup page). So, Biconomy is the submitter (on behalf of SC owner/deployer) of the signed message by caller (user).
- Here, Biconomy has deployed a
-
Add DApp: name, sc_address, sc_abi
-
Go to "Manage API": add DApp by name (filter), then select the function for which this gasless feature (meta-transaction) has to be enabled.
-
Use the generated API during Biconomy SDK installation inside the code (FE, BE) via
$ npm install @biconomy/mexa
& then add this code:
import {Biconomy} from "@biconomy/mexa";
const biconomy = new Biconomy(<web3 provider>, {apikey: <API Key>});
web3 = new Web3(biconomy);
biconomy.onEvent(biconomy.READY, ()=> {
// initialize your dApp here like getting user accounts etc.
}).onEvent(biconomy.ERROR, (error, message) => {
// Handle error while initializing mexa
});
Read more.
Q. Why Limit?
A. This is to prevent DoS attacks. If a big contract is deployed on-chain, then during the call of SC, the node (which adds the transaction into block) has to do the computation (based on the optimizer run's value). So, more the optimizer run's value => higher is the contract size => lesser would be the gas fees (more optimized). Hence, it is advised to set the optimizer close to 200 optimum value. Hence, a programmer can write a long SC that is very cheap to call and can make the nodes do a lot of work, which may cause blockage.
These are some ways to reduce the contract size:
- Directly call the variable. Don't define the variable, unless it's used multiple times in a function. Source.
- replace assertion message with error code like this:
function stake() {
require(msg.sender == owner, "Only owner can stake");
}
// ERR1: Only owner can stake
function stake() {
require(msg.sender == owner, "ERR1");
}
- Reducing Optimizer Run: Reducing the no. of runs in the optimizer decreases the contract size but then leads to high call cost. So, if your deployed SC is to be called a few times or you don't care about the user's pocket, you can reduce the runs or disable the optimizer. So, by default it should be set as 200 which is considered optimum value. Rest can be tuned as per your application.
- In Diamond Standard (DS), separate out the function from a facet to a new facet. Anyways, the facets address are mapped to function signature.
- wrap the code present in function modifier with a function (private, view) outside & just call the function inside the modifier. Source.
- Disable
yul
in hardhat ‘optimizer’ inhardhat.config.ts
file. Source - Manually found the function which caused this via "comment & compile" for each function in a Solidity (contract) file in the repo. When function is found, then check for 2 things:
- Change function local variables type from
calldata
tomemory
. Source - Shorten the function size by cascading method. It could be that the function is using more than 16 (max.) local variables including input, output, inside arguments. Break down
f1
(responsible function) intof1
,f2
,f3
& use inside of each other:f1 <— f2 <— f3
- Change function local variables type from
- Although using Diamond Standard (DS), the chances of reaching the size limit is less. But, still it might be due to usage of
FacetCut[] memory _diamondCut
in the constructor. So, an alternative method is to try for another method. Source - Try to call the state variable directly rather than calling via
view
,public
function like in this case:
function createCollectible(string memory _tokenURI)
external
returns (uint256)
{
uint256 _newTokenId = _tokenCounter;
_safeMint(msg.sender, _newTokenId);
_setTokenURI(_newTokenId, _tokenURI);
++_tokenCounter;
emit CollectibleMinted(msg.sender, _newTokenId);
return _newTokenId;
}
function getNextTokenId() external view returns (uint256) {
return _tokenCounter;
}
In this case, the function
getNextTokenId
is defined asexternal
& in the line:uint256 _newTokenId = _tokenCounter;
the state variable is called directly insidecreateCollectible
function.
- Use
pragma experimental ABIEncoderV2;
to reduce the size of the contract. Source - Avoid using additional variables in the function like this:
❌
function get(uint id) returns (address,address) {
MyStruct memory myStruct = myStructs[id];
return (myStruct.addr1, myStruct.addr2);
}
✅
function get(uint id) returns (address,address) {
return (myStructs[id].addr1, myStructs[id].addr2);
}
Size got reduced by 0.28 KB
in this case.
- Use
error
keyword instead of string message inrevert
statement like this:
error InsufficientBalance;
// error InsufficientBalance(uint requested, uint available);
if balance < amount {
revert InsufficientBalance;
// revert InsufficientBalance(amount, balance);
}
There is a dilemma b/w contract size & gas cost. So, we need to find the optimum value of the optimizer. The following table shows the relationship between the optimizer value & contract size & gas cost.
optimizer value ⬇️ => contract size ⬇️
optimizer value ⬇️ => gas cost ⬆️
- constant variable can be defined like this:
uint256 constant INITIAL_RATE = 2_474_410 \* 10 \*\* 18 / WEEK;
- By default, the variables are private (i.e. not accessed from external).
Note: It's actually not private storage var, as ETH is a public blockchain.
- For array variable declarations, the parentheses in types and arrays cannot have spaces directly.
// The way to standardize:
uint[] x;
// ❌ Unregulated way:
uint [] x;
- There must be a space on both sides of the assignment operator
// The way to standardize:
x = 3;x = 100 / 10;x += 3 + 4;x |= y && z;
// ❌ Unregulated way:
x=3;x = 100/10;x += 3+4;x |= y&&z;
- In order to display priority, there must be spaces between the precedence operator and the low priority operator, which is also to improve the readability of complex declarations. The number of spaces on either side of the operator must be the same.
// The way to standardize:
x = 2**3 + 5;x = 2\***y + 3*z;x = (a+b) * (a-\*\*b);
// ❌ Unregulated way:
x = 2\*\* 3 + 5;x = y+z;x +=1;
- Visibility: private, public, internal
- More style guides
Enums cannot have more than 256 members
- Statement initialscapital, define the first letter of the enum enumeration variablelower case,Such as:
// Game status
enum GameState {
GameStart, // Game starts
InGaming, // In game
GameOver // Game is over
}
GameState public gameState; // The state of the current game
- The constructor is a special function run during the creation of the contract and you cannot call it afterwards.
- Syntax
constructor() <functionModifiers> {}
// Code
}
- A constructor can only use the public or internal function modifiers.
function <function name> (<parameter types>)
[internal | external | public | private]
[pure | constant | view | payable]
[modifiers]
[returns (<return types>)]
{
<body>
}
- cannot give the paramter the same name as a state variable.
- by default, functions are internal, so no need to write anything, or else, mention
public
- Getter methods are marked
view
. view
&pure
are used to describe a function that does not modify the contract's state.constant
on functions is an alias toview
, but this is deprecated and is planned to be dropped in version 0.5.0.constant
/view
functions are free to access.- Functions can be declared pure in which case they promise not to read from or modify the state.
- Overloading is possible with multiple functions named same with different params.
- function visibility
public - all can access
external - Cannot be accessed internally, only externally
internal - only this contract and contracts deriving from it can access
private - can be accessed only from this contract
- In private access, the function is defined by prefixing underscore
_
. E.g.function _getValue() returns(uint) { }
. Also, the function is no more visible in the IDE (e.g. Try in Remix) - multiple output function
// M-1
function getValue() external view returns(address, address) {
return (tx.origin, msg.sender);
}
// M-2
function getValue2() external pure returns(uint sum, uint product) {
uint v1 = 1;
uint v2 = 2;
sum = v1 + v2;
product = v1 * v2;
return (sum, product);
}
- For function declarations with more parameters, all parameters can be displayed line by line and remain the same indentation. The right parenthesis of the function declaration is placed on the same line as the left parenthesis of the function body, and remains the same indentation as the function declaration.
The way to standardize:
function thisFunctionHasLotsOfArguments(
address a,
address b,
address c,
address d,
address e,
address f
) {
do_something;
}
❌ Unregulated way:
function thisFunctionHasLotsOfArguments(address a, address b, address c,
address d, address e, address f) {
do_something;
}
- If the function includes multiple modifiers, you need to branch the modifiers and indent them line by line. The left parenthesis of the function body is also branched.
The way to standardize:
function thisFunctionNameIsReallyLong(address x, address y, address z)
public
onlyowner
priced
returns (address)
{
do_something;
}
❌ Unregulated way:
function thisFunctionNameIsReallyLong(address x, address y, address z)
public onlyowner priced returns (address){
do_something;
}
- For a derived contract that requires a parameter as a constructor, if the function declaration is too long or difficult to read, it is recommended to display the constructor of the base class in its constructor independently.
The way to standardize:
contract A is B, C, D {
function A(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4)
{
// do something with param5
}
}
❌ Unregulated way:
contract A is B, C, D {
function A(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4)
{
// do something with param5
}
}
-
Fallback functions
// This fallback function // will keep all the Ether function() public payable { balance[msg.sender] += msg.value; }
- The solidity fallback function is executed if none of the other functions match the function identifier or no data was provided with the function call.
- contracts can have one unnamed function
- Can not return anything.
- It is mandatory to mark it external.
- It is limited to
2300
gas when called by another function. It is so for as to make this function call as cheap as possible.
-
NOTE: Because they don't modify the state, view and pure functions do not have a gas cost - which is to say they are FREE!
-
Function signature: E.g.
setCompleted(uint256)
-
Function selector: E.g. 0x
fdacd576
i.e. keccak256("setCompleted(uint256)")[:4] -
Every function has a signature whose 1st 4 bytes are considered as "function selector" like 0x
fdacd576
. It is formed via hashing a contract's function with name & argument type like thissetCompleted(uint256)
Source. -
Using
foundry
tool, we can generate function selector for a function.
cast sig "transfer(address,uint256)"
-
2 functions having same code logic i.e. same bytecode may end up having different gas cost. This is because of their position/index in the contract. The function with lower index will have lower gas cost. Source. This is the way the functions are stored in the contract. The function with lower index will be stored first and the function with higher index will be stored later. So, the function with lower index will have lower gas cost.
func1 func2
Here, same code logic. But,
func1
will have lower gas cost because in order to queryfunc1
, we need to query the first 4 bytes of the contract. So, it will be cheaper to queryfunc1
.
- Constant definitions are all used capitalEasy to distinguish from variables and function parameters, such as:
uint256 constant public ENTRANCE_FEE = 1 ether; // admission fee
- Contracts can emit events on the Blockchain that Ethereum clients such as web applications can listen for without much cost. As soon as the event is emitted, the listener receives any arguments sent with it and can react accordingly.
- It's also a way to print debug. Although this can also be done using
console.sol
using Hardhat. - Blockchain nodes can subscribe to events of a contract.
- Syntax
// create event
event <eventName>(<List of parameters and types to send with event>);
// emit event
emit <eventName>(<List of variables to send>);
- Statement initialscapital, variable initialslower case, send event to add keywordsemit,Such as:
event Deposit(
Address from, // transfer address
Uint amount // transfer amount
);
function() public payable {
emit Deposit(msg.sender, msg.value);
}
- indexing a field inside an event. This is done using
indexed
, shown here. - Max. 3 indexing can be done.
- Events can't be read from smart contract. This happens from blockchain to the outside world.
- Events consume very less gas, as they are not
storage
variables. - The common uses for events can be broken down into three main use cases:
- Events can provide smart contract return values for the User Interface
- They can act as asynchronous triggers with data and
- They can act a cheaper form of storage.
- Logs cost 8 gas per byte whereas contract storage costs 20,000 per 32 bytes, or 625 gas per byte.
- Events are inheritable members of contracts. You can call events of parent contracts from within child contracts.
- Remember that events are not emitted until the transaction has been successfully mined.
- Logging an event for every state change of the contract is a good heuristic for when you should use events. This allows you to track any and all updates to the state of the contract by setting up event watchers in your javascript files.
NOTE: Inside a function, events are emitted before the return statement like this:
// deploy a new purchase contract
function newPurchase()
public
payable
returns(address newContract)
{
Purchase c = (new Purchase).value(msg.value)(address(msg.sender));
contracts.push(c);
lastContractAddress = address(c);
emit newPurchaseContract(c);
return c;
}
// according to roomId => gameId => playerId => Player
mapping (uint => mapping (uint => mapping (uint => Player))) public players;
- Storage keywords in Solidity is analogous to Computer’s hard drive.
- Storage holds data between function calls.
- State variables and Local Variables of structs, array, mapping are always stored in storage by default.
- Storage on the other hand is persistent, each execution of the Smart contract has access to the data previously stored on the storage area.
For more, read these
In case of selection between
gas optimization
,contract size
. Put this as priority:gas optimization
>contract size
.
- Memory keyword in Solidity is analogous to Computer’s RAM.
- Much like RAM, Memory in Solidity is a temporary place to store data
- The Solidity Smart Contract can use any amount of memory during the execution but once the execution stops, the Memory is completely wiped off for the next execution.
- Function parameters including return parameters are stored in the memory.
- Whenever a new instance of an array is created using the keyword ‘memory’, a new copy of that variable is created. Changing the array value of the new instance does not affect the original array.
- Therefore, it is always better to use Memory for intermediate calculations and store the final result in Storage.
- The memory location is temporary data and cheaper than the storage location.
- Usually, Memory data is used to save temporary variables for calculation during function execution.
- Local variables with a value type are stored in the memory. However, for a reference type, you need to specify the data location explicitly. Local variables with value types cannot be overriden explicitly.
function doSomething() public {
/* these all are local variables */
bool memory flag2; //error
uint Storage number2; // error
address account2;
}
- Any local variable or function argument in a function, which has size > 32 bytes need to be defined with memory/calldata type. Bcoz there is temp. data persistence required.
E.g.
struct memory cart
string calldata name
address[] calldata receivers
calldata
is non-modifiable and non-persistent data location where the function params (args) are stored. Also,calldata
is the default location of parameters (not return parameters) of external (≤0.6.9
version) functions, but now all (≥0.7.0
) visibility functions.- So, use as a function parameters for a function of any visibility for solidity version ≥
0.7.0
- Don't use for local variable to be used inside a function. Instead use memory.
calldata
is read-only, mainly used for (external)function params.
- Mappings act as hash tables which consist of key types and corresponding value type pairs.
- Mappings types allow you to create your own custom types, consisting of key/value pairs. Both the key and the value can be any type.
- Syntax
mapping (<key> => <value>) <modifiers> <mappingName>;
- key data is not stored in the mapping, rather its keccack256 hash.
- A mapping declared public will create a getter requiring the
_keyType
as a parameter and return the_valueType
. - When mappings are initialized every possible key exists in the mappings and are mapped to values whose byte-representations are all zeros.
- can't be iterated across the keys unlike arrays. But, can be iterated across keys by storing the keys into a separate state var arrays of keys.
mapping(address => User) userList2;
// uint mappingLen; // M-1, cons: getting only length, but not able to iterate across keys
address[] mappingKeyArr; // M-2
- Another way to find whether a value exist for a key is given here
- Example - Mapping.sol
- check if key exists:
if (abi.encodePacked(balances[addr]).length > 0) {
delete balances[addr];
}
- Mapping length is missing, not multi-index directly, but can be made as multi-index by keeping the value as struct of many fields.
- get length of the mapping:
- whenever add the element, try to add a key_counter or an array holding the keys;
- that's how, the counter value or the length of the array is the length of the mapping.
- delete key:
delete balances[addr]
- Use cases:
- blockchain-based puzzle game
- a blockchain-based puzzle game that manages user state and ETH payments to players using smart contracts
- blockchain-based puzzle game
- It is present in storage always, & passed by reference whenever called.
- When using
mapping
, always remember to fire an event that shows the key-value pair. So that in the future, if one needs to check all the populated keys, they can refer to the event log (on etherscan). - We can store the keys into an array.
The issue with storing an array on-chain is that it will be extremely expensive, and if you needed to change one of the values you'd have to remember the index that you want to change. If you don't already know the value, you'd have to loop through the entire array to find it. You could return the array and loop through it off-chain then pass in the index, but that also leaves room for error if there's a lot of activity in the app since the position of the values in the array could change by then.
- delete at an index using
delete myArray[3]
- delete the last element using
delete myArray[myArray.length-1]
- If you start with an array [A,B,C,D,E,F,G] and you delete "D", then you will have an array [A,B,C,nothing,E,F,G]. It's no shorter than before.
- Get all elements:
function getAllElement() public view returns (uint[]) {
return arr;
}
- test array variable
assert(a[6] == 9);
- pop element
function popElement() public returns (uint []){
delete arr[arr.length-1];
arr.length--;
return arr;
}
- get size/length of array using
arr.length
- It is present in storage always, & passed by reference whenever called.
- They can have only fields, but not methods.
- Example
- definition
struct User {
address addr;
uint score;
string name;
}
// here, memory/storage can be used as per the requirement. `memory` is used here as it is not required to be stored & computation happening within the function itself.
function foo(string calldata \_name) external {
User memory u1 = User(msg.sender, 0, \_name);
User memory u2 = User({name: \_name, score: 0, addr: msg.sender}) // Pros: no need to remember the order. Cons: write little more variables
// access the variables
u1.addr;
// update
u1.score = 20;
// delete
delete u1;
}
- It is present in storage always, & passed by reference whenever called.
- It creates a pointer 'c' referencing a variable in storage.
// Campaign is a struct
// campaigns is an array
Campaign storage c = campaigns[campaignID];
- directly it's not possible like in EOSIO using
eosio::multi_index
, but by creating amapping
with values type asstruct
& then get features like:- to store the length of array &
- also iterate across keys
-
If your SC has a deposit function for native coin (like ETH), then use these functions in order to revert your transactions like:
receive() external payable {
revert("Can NOT send native coin")
}
fallback() external payable {
revert("Can NOT send native coin")
}
- example
send
,transfer
is avoided as per latestv0.8.6
, rather.call()
is preferred
- example
- No need to declare
payable
for thedeposit
function
- Example
- Modifier definition useHump nomenclature,Initialslower case,Such as:
modifier onlyOwner {
require (msg.sender == owner, "OnlyOwner methods called by non-owner.");
_;
}
- The default modifier should be placed before other custom modifiers.
The way to standardize:
function kill() public onlyowner {
selfdestruct(owner);
}
❌ Unregulated way:
function kill() onlyowner public {
selfdestruct(owner);
}
- Example:
modifier modi() {
prolog();
_;
epilog();
}
function func() modi() {
stuff();
}
is equivalent to
function func() {
prolog();
stuff();
epilog();
}
- In the above example, if the stuff() includes any external call (say
call
,delegatecall
), then theepilog()
of the modifier is executed only after the whole functionfunc()
is executed. Watch this Understanding Reentrancy modifier execution
- Member types
address payable
: Same asaddress
, but with the additional memberstransfer
andsend
- The idea behind this distinction is that
address payable
is an address you can send Ether to, while a plainaddress
cannot be sent Ether. - Implicit conversions from
address payable
toaddress
are allowed, whereas conversions fromaddress
toaddress payable
must be explicit viapayable(<address>)
- If you need a variable of type
address
and plan to send Ether to it, then declare its type asaddress payable
to make this requirement visible. Also, try to make this distinction or conversion as early as possible. transfer
is much safer thansend
, as the former throws an exception. And both has gas limit of 2300 gastransfer
(throws exception):
<address>.transfer(amount);
send
(returnbool
type):
bool success = <address>.send(amount);
if(!success) {
// deal with the failure case
} else {
// deal with the success case
}
- Solidity uses state-reverting exceptions to handle errors. Such an exception undoes all changes made to the state in the current call (and all its sub-calls) and flags an error to the caller.
- 3 convenience functions:
assert
require
revert
- Syntax for
require
require(<logicalCheck>, <optionalErrorMessage>);
- Example 1: Base Caller Contracts
- Example 2: Context Switcher
- call, delegatecall
- Example 3: Delegate Call
- calling a contract function with multiple arguments:
// w/o gas limit
x.call(abi.encodePacked(bytes4(keccak256("setNum(uint256,string,address)")), myUIntVal, myStringVal, myAddressVal));
// with gas limit (in Wei)
x.call.value(1000)(abi.encodePacked(bytes4(keccak256("setNum(uint256,string,address)")), myUIntVal, myStringVal, myAddressVal));
- While the clock on a computer ticks at least once a millisecond, the clock on a blockchain only ticks as often as blocks are added to the chain.
- In the following code, the time attribute would be the same for all events emitted by this function as it is set by block.timestamp.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Time {
event TimeLog(uint256 time);
function reportTime() public {
for(uint8 iterator; iterator < 10; iterator++){
emit TimeLog(block.timestamp);
}
}
}
- The
block.timestamp
returns the current block timestamp in seconds since the UNIX epoch as aunit256
number. - As a result, the block.timestamp property will be identical for each transaction on the block.
- We can never expect an exact second due to the low-resolution clock of blockchain; therefore, our time comparison should always include greater or less than, rather than equal.
- It’s also worth remembering that block creators can influence the time a block is created and the order in which transactions are processed to their benefit, leading to a Front-Running attack, a known Ethereum protocol issue.
- In conclusion, it would be prudent not to take non-trivial decisions based on the time provided by the blockchain. When comparing time instead of exact seconds, use greater than or less than, but not equal to.
- EIP-20: Token Standard
- EIP-165: Standard Interface Detection
- EIP-712: Ethereum typed structured data hashing and signing
- EIP-721: Non-Fungible Token Standard
- EIP-1155: Multi Token Standard
- EIP-2535: Diamonds, Multi-Facet Proxy
- EIP-2612: Permit Extension for EIP-20 Signed Approvals
- Use solmate ERC20.sol (includes
permit()
function)
- Use solmate ERC20.sol (includes
- EIP-2771: Secure Protocol for Native Meta Transactions
- EIP-3156: Flash Loans
- EIP-3525: Semi-Fungible Token Standard
- EIP-4675: Multi-Fractional Non-Fungible Tokens
- Libraries are contracts that do not have storage, they cannot hold ether.
- They cannot have state variables
- They cannot inherit or be inherited by other contracts.
- Libraries can be seen as implicit base contracts of the contracts that use them.
- They exist for the purpose of code reuse.
- They cannot receive Ether.
- They cannot be destroyed.
- Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the
DELEGATECALL
(CALLCODE
until Homestead) feature of the EVM. This means that if library functions are called, their code is executed in the context of the calling contract, i.e.this
points to the calling contract, and especially the storage from the calling contract can be accessed. - Contracts can call library functions without having to implement or deploy the functions for itself - allowing the library functions to modify the state of the calling contract.
- This is made possible by the DELEGATECALL opcode of the EVM. This enables developers to use code that has already been audited and battle-tested in the wild.
- A caveat - calling a library function from a contract is a bit more expensive than calling internal functions, so there is a trade-off to consider. If the contract functions calling the library are frequently called, it may be better to pay the higher deployment cost to get cheaper function calls. You will have to run tests to determine which is best for your use case.
- To connect to a library, you need the library contract as well as the address of the deployed instance.
- The directive
using A for B;
can be used to attach library functions (from the library A) to any type (B) in the context of a contract. - The effect of
using A for *;
is that the functions from the libraryA
are attached to any type.
- There are special variables and functions which always exist in the global namespace and are mainly used to provide information about the blockchain or are general-use utility functions
blockhash(uint blockNumber) returns (bytes32): hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero
block.chainid (uint): current chain id
block.coinbase (address payable): current block miner’s address
block.difficulty (uint): current block difficulty
block.gaslimit (uint): current block gaslimit
block.number (uint): current block number
block.timestamp (uint): current block timestamp as seconds since unix epoch
gasleft() returns (uint256): remaining gas
msg.data (bytes calldata): complete calldata
msg.sender (address): sender of the message (current call)
msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
msg.value (uint): number of wei sent with the message
tx.gasprice (uint): gas price of the transaction
tx.origin (address): sender of the transaction (full call chain)
- In Solidity, when a function assigns a new value to a state variable and then returns that variable, the returned value reflects the newly assigned value.
function setNum(uint256 _num) public returns (uint256) {
num = _num;
return num;
}
The num
state variable is first updated with the _num
. Immediately after, the function returns the current value of num
, which is the newly assigned result. Therefore, the function returns the updated value of num
, not its previous value.
The arithmetic operation is very tricky (in terms of precision handling especially in DeFi applications) in Solidity. The following code will give an idea of precision.
In Solidity, the order of the arithmetic operations is not clearly defined b/w same levels like multiplication and division. So, it is always better to use parentheses to avoid any precision loss (eventually) & clarify the operation, in addition to following multiply first and then divide to avoid precision loss.
// bracketing `accruedInterestOfprevDepositedAmt / scalingFactor` is optional
vaults[msg.sender] = _lastDepositBalanceOf + (accruedInterestOfprevDepositedAmt / scalingFactor) + _amount;
Always check the arithmetic operations (at testing level) by altering the operands and operators using Foundry.
contract Fee {
/// @dev precision loss. division first
function caculateLowFee() public pure returns (uint256) {
uint256 coins = 2;
uint256 Total_coins = 10;
uint256 fee = 15;
return ((coins / Total_coins) * fee);
}
/// @dev no precision loss. multiplication first
function caculateHighFee() public pure returns (uint256) {
uint256 coins = 2;
uint256 Total_coins = 10;
uint256 fee = 15;
return ((coins * fee) / Total_coins);
}
}
Key lessons:
-
Multiply first and then divide. This will avoid precision loss as the numerator would be very big enough to be divided especially when the divisor is a big number (than numerator). This is possible when the divisor type is
uint256
and the numerator type isuint128
.larger-bit/smaller-bit
....is good thansmaller-bit/larger-bit
(it would be zero) in solidity. -
E.g.
uint256 / uint32 👍 uint32 / uint256 👎
-
Similarly, b/w addition & subtraction, addition first and then subtraction in order to avoid negative (
-
) value as result. -
The calculated result for division and multiplication can be stored in an integer with more bits, but the operands must also be integers of the same size. There can be cases like in autocompounding case, there were multiple times alteration in order to use
PPFS
at the numerator or denominator in different situations. The solution is for each use, consider a numerator & denominator pair (don't use any pre-calculated value from a dependent function). This will avoid precision loss. E.g.x * (1 + x/y)
. should not be treated like numerator as(x * (y + x))
and denominator asy
. This might lead to arithmetic overflow/underflow, division by zero, etc. -
In autocompound vault example, during
receiptAmt
calculation, tried withnum/denom
approach, but failed with arithmetic error. Code that failed is:uint256 numerator = _amount * 1e18 * yieldDuration * scalingFactor * totalShares(); uint256 denominator = _totalDeposited * 1e18 * (yieldDuration * scalingFactor + (block.timestamp - lastDepositedTimestamp) * yieldPercentage); uint256 receiptAmt = numerator / denominator;
LESSON:
x + y/z
can't be treated as(z+y)/z
. We need to choose b/w multiplication (1st) vs division (2nd). Don't complicate with '+', '-'.Finally, went ahead with the following code:
uint256 receiptAmt = _amount * 1e18 / getPPFS(); // more precision // uint256 receiptAmt = _amount / (ppfs / 1e18); // less precision
Read more
-
How to ensure we have max. decimal places. In order to do that we have to set the
scaling_factor
carefully. Suppose, we want to use yield percentage as0.000001
, then take thescaling_factor
as1000000
(consider bare min. possible, don't use1e18
as might lead to precision loss) and then multiply the yield percentage withscaling_factor
which results to1
value and then use it.// For `yield_percentage`, consider this: 0.000001% to 100% value when multiplied with // scaling_factor....for setting the data type - uint32/uint64/uint128/uint256 uint32 public yield_percentage; uint32 public yield_duration; // in seconds uint32 public constant scaling_factor = 1000000;
Now, when we want to calculate the accrued interest on staked amount, then we have to divide the accrued interest with
scaling_factor
to get the actual value i.e.(amount * yield_percentage) / scaling_factor
. -
The operands for the exponentiation function must be unsigned integers. Unsigned Integers with lower bits can be calculated and stored as unsigned integers with higher bits.
-
To apply an arithmetic operation to all of the operands, they must all have the same data type; otherwise, the operation will not be performed.
- Formatting | Avoid parentheses, brackets, and spaces after curly braces
The way to standardize:
spam(ham[1], Coin({name: “ham”}));
❌ Unregulated way:
spam( ham[ 1 ], Coin( { name: “ham” } ) );
- Formatting | Avoid spaces before commas and semicolons
The way to standardize:
function spam(uint i, Coin coin);
❌ Unregulated way:
function spam(uint i , Coin coin) ;
- Formatting | Avoid multiple spaces before and after the assignment
The way to standardize:
x = 1;
y = 2;
long_variable = 3;
❌ Unregulated way:
x = 1;
y = 2;
long_variable = 3;
- Formatting | Control structure
The way to standardize:
contract Coin {
struct Bank {
address owner;
uint balance;
}
}
❌ Unregulated way:
contract Coin
{
struct Bank {
address owner;
uint balance;
}
}
- Formatting | For the control structure, if there is only a single statement, you don't need to use parentheses.
The way to standardize:
if (x < 10)
x += 1;
❌ Unregulated way:
if (x < 10)
someArray.push(Coin({
name: 'spam',
value: 42
}));
- Storage vs Memory | Wrong way to use
storage
,memory
: Here, State variables are always stored in thestorage
. Also, you can not explicitly override the location of state variables.
pragma solidity ^0.5.0;
contract DataLocation {
//storage
uint stateVariable;
uint[] stateArray;
}
❌ Unregulated way:
pragma solidity ^0.5.0;
contract DataLocation {
uint storage stateVariable; // error
uint[] memory stateArray; // error
}
-
Arithmetic Operation | Don't use BODMAS rule in Solidity. It is not applicable in Solidity. Instead, use
()
to make the order of operation clear & also prioritize multiplication over division. (as multiplication, division are put on same level of precedence). More.
- fixed-size types
bool isReady;
uint a; // uint alias for uint256
address recipient;
bytes32 data;
- variable-size types
string name;
bytes _data;
uint[] amounts;
mapping(uint => string) users;
- user-defined data
struct User {
uint id;
string name;
uint[] friendIds;
}
enum {
RED,
BLUE,
GREEN
}
- Main global variables:
block
,msg
,tx
- Instead of
string
,bytes32
data type is used for security reasons & also to save memory. This is because, in ASCII encoding, each character needs 8 bits, whereas in Unicode encoding, each character needs 16 bits- E.g. “Hello World”, ASCII size = ( 11 8)/8 = 11 Bytes & Unicode size = ( 11 16)/8 = 22 Bytes.
- Then there are language specific things that get added up to these. For example in C, we will need an ‘\0’ at end of each string(char array), so we will need an extra byte.
- Unicode is widely used these days, as it supports multiple languages and emotions to be represented.
- Which one to use
external
orpublic
?- depends on what consumes more gas
- with the latest solidity version 0.8.4:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
contract ExternalPublicTest {
function test(uint[20] memory a) public pure returns (uint){
return a[10]*2;
}
function test2(uint[20] calldata a) public pure returns (uint){
return a[10]*2;
}
}
-
It's actually about
memory
orcalldata
. The former would consume more gas (491 wei) & the later would consume 260 wei gas.But, if the var is modified, then can't define it as
calldata
. -
now
-> which is equivalent toblock.timestamp
may not be as accurate as one may think. It is up to the miner to pick it, so it could be up to 15 minutes (900 seconds) off. -
view
vspure
in functionview
demo: Here, the function is making a change (optional) into the state variables num1, num2 & getting the output.
// Solidity program to // demonstrate view // functions pragma solidity ^0.5.0; // Defining a contract contract Test { // Declaring state // variables uint num1 = 2; uint num2 = 4; // Defining view function to // calculate product and sum // of 2 numbers function getResult( ) public view returns( uint product, uint sum){ uint num1 = 10; uint num2 = 16; product = num1 * num2; sum = num1 + num2; } }
pure
demo: Here, the function won't be able to read the state variables num1, num2 or even modify num1, num2, but getting the output.
// Solidity program to // demonstrate pure functions pragma solidity ^0.5.0; // Defining a contract contract Test { // Defining pure function to // calculate product and sum // of 2 numbers function getResult( ) public pure returns( uint product, uint sum){ uint num1 = 2; uint num2 = 4; product = num1 * num2; sum = num1 + num2; } }
-
check if the address is present
// 1. store the addresses in a mapping
mapping(address => uint256) mapAddressBool;
// 2. check if the address is blocked
require(mapAddressBool[addr] == 0, "the address is not blocked");
- It makes the contract inoperable.
- Using 2
require
statements is less costly than 1require
statement with&&
.
require(_stf.fundraisingPeriod >= 15 minutes, "Fundraising should be >= 15 mins");
require(_stf.fundraisingPeriod <= maxFundraisingPeriod, "Fundraising should be <= a week");
is better than
require((_stf.fundraisingPeriod >= 15 minutes) && (_stf.fundraisingPeriod <= maxFundraisingPeriod), "Fundraising should be b/w 15 mins to a week");
-
Variable packing:
- Solidity stores data in 256-bit memory slots. Variables less than 256 bits will be stored in a single slot, Data that does not fit in a single slot is spread over several slots.
- Each storage slot costs gas, packing the variables helps you optimize your gas usage by reducing the number of slots our contract requires. Image
-
Turn-on Solidity Optimizer:
- specify an optimization flag to tell the Solidity compiler to produce highly optimized bytecode. Image
-
Delete variables that you don’t need:
- In Ethereum, you get a gas refund for freeing up storage space.
- Deleting a variable refund 15,000 gas up to a maximum of half the gas cost of the transaction. Deleting with the
delete
keyword is equivalent to assigning the initial value for the data type, such as0
for integers.
-
Compute known value-off chain:
- If you know what data to hash, there is no need to consume more computational power to hash it using
keccak256
, you’ll end up consuming 2x amount of gas. Image
- If you know what data to hash, there is no need to consume more computational power to hash it using
-
Do not shrink Variables:
- If only
uint8
,uint16
,uint32
, etc. are used as a state variables, then there is going to be gas consumed in converting it into256 bit
. So, it's better if it's already defined asuint256
- In solidity, you can pack multiple small variables into one slot, but if you are defining a lone variable and can’t pack it, it’s optimal to use a
uint256
rather thanuint8
.
- If only
-
Data location:
- Variable packing only occurs in storage — memory and call data does not get packed. You will not save space trying to pack function arguments or local variables.
-
Reference data types:
- Structs and arrays always begin in a new storage slot — however their contents can be packed normally. A uint8 array will take up less space than an equal length uint256 array.
- It is more gas efficient to initialize a tightly packed struct with separate assignments instead of a single assignment. Separate assignments makes it easier for the optimizer to update all the variables at once.
- Initialize structs like this:
Point storage p = Point() p.x = 0; p.y = 0;
instead of:
Point storage p = Point(0, 0);
-
Inheritance
- When we extend a contract, the variables in the child can be packed with the variables in the parent.
- The order of variables is determined by C3 linearization. For most applications, all you need to know is that child variables come after parent variables.
-
Use Events:
- Data that does not need to be accessed on-chain can be stored in events to save gas.
- While this technique can work, it is not recommended — events are not meant for data storage. If the data we need is stored in an event emitted a long time ago, retrieving it can be too time consuming because of the number of blocks we need to search.
-
User Assembly:
- When you compile a Solidity smart contract, it is transformed into a series of EVM (Ethereum virtual machine) opcodes.
- With assembly, you write code very close to the opcode level. It’s not very easy to write code at such a low level, but the benefit is that you can manually optimize the opcode and outperform Solidity bytecode in some cases.
-
Use Libraries:
- If you have several contracts that use the same functionalities, you can extract these common functions into a single library, and then you’re gonna deploy this library just once and all your contracts will point to this library to execute the shared functionalities.
-
Minimize on-chain data:
- The less you put on-chain, the less your gas costs.
- When you design a Dapp you don’t have to put 100% of your data on the blockchain, usually, you have part of the system (Unnecessary data (metadata, etc .. ) ) on a centralized server.
-
Avoid manipulating storage data
- Performing operations on memory or call data, which is similar to memory is always cheaper than storage. Image
- In the Second contract, before running the for loop we’re assigning the value of a storage data d to
_d
to avoid accessing the storage each time we iterate. - A common way to reduce the number of storage operations is manipulating a local memory variable before assigning it to a storage variable.
- We see this often in loops:
uint256 return = 5; // assume 2 decimal places
uint256 totalReturn;
function updateTotalReturn(uint256 timesteps) external {
uint256 r = totalReturn || 1;
for (uint256 i = 0; i < timesteps; i++) {
r = r * return;
}
totalReturn = r;
}
- In `updateTotalReturn`, we use the local memory variable `r` to store intermediate values and assign the final value to our storage variable `totalReturn`.
- This reporter displays gas consumption changes to each function in your smart contract.
- Use Short-Circuiting rules to your advantage:
- When using logical disjunction (||), logical conjunction (&&), make sure to order your functions correctly for optimal gas usage.
- In logical disjunction (OR), if the first function resolves to true, the second one won’t be executed and hence save you gas.
- In logical disjunction (AND), if the first function evaluates to false, the next function won’t be evaluated. Therefore, you should order your functions accordingly in your solidity code to reduce the probability of needing to evaluate the second function.
- Use
ERC1167
To Deploy the same Contract many time- EIP1167 minimal proxy contract is a standardized, gas-efficient way to deploy a bunch of contract clones from a factory.EIP1167 not only minimizes length, but it is also literally a “minimal” proxy that does nothing but proxying. It minimizes trust. Unlike other upgradable proxy contracts that rely on the honesty of their administrator (who can change the implementation), the address in EIP1167 is hardcoded in bytecode and remain unchangeable.
- Avoid assigning values that You’ll never use:
- Every variable assignment in Solidity costs gas. When initializing variables, we often waste gas by assigning default values that will never be used.
uint256 value;
is cheaper thanuint256 value = 0;
.
- Use Mappings instead of Arrays:
- Solidity is the first language in which mappings are less expensive than arrays.
- Most of the time it will be better to use a
mapping
instead of an array because of its cheaper operations.
- Limit the string length in the Require Statements
require()
- define
strings
asbytes32
- define
- Fixed-size Arrays are cheaper than dynamic ones:
- If we know how long an array should be, we specify a fixed size:
uint256[12] monthlyTransfers;
- This same rule applies to strings. A
string
orbytes
variable is dynamically sized; we should use abytes32
if our string is short enough to fit. - If we absolutely need a dynamic array, it is best to structure our functions to be additive instead of subtractive. Extending an array costs constant gas whereas truncating an array costs linear gas.
- If we know how long an array should be, we specify a fixed size:
- In solidity version
^0.8.0
, useunchecked
to skip the validation check i.e. overflow/underflow in order to save gas. So, the unnecessary checks are no more needed if we are sure about the variable. E.g:
require(balance < value, "balance too high");
unchecked {
balance += value;
}
// OR
require(balance > value, "balance too low");
unchecked {
balance -= value;
}
-
For-loops can also be written like this using
unchecked
to save additional computation cost for checking overflow/underflow, if the array is of fixed size.- Before:
for (uint i = 0; i < uw.length; ++i) { Trove memory t = users[uw[i]]; // add reward to user's trove users[uw[i]].rewardedAmt += _getUSDCReward(t.depositedAmt); }
- After:
for (uint i = 0; i < uw.length; ) { Trove memory t = users[uw[i]]; // add reward to user's trove users[uw[i]].rewardedAmt += _getUSDCReward(t.depositedAmt); unchecked { ++i; } }
-
Don't use
safe
methods for token transfer likesafeTransferFrom
when the contract is receiving the token. It saves additional gas.- Before:
IERC20(USDCToken).safeTransferFrom(msg.sender, address(this), _amount);
- After:
IERC20(USDCToken).transferFrom(msg.sender, address(this), _amount);
- Refer
- Images
- Two expensive functions:
SSTORE
(AKA, “Store this data in this storage slot”)SLOAD
(AKA, “Load the data from this slot into memory”)
- Summary:
- Don’t Store if You Don’t Have To
- Use Constants and Immutables
- Make it Obvious You’re Touching Storage
- Don’t Read and Write Too Often
- Pack Your Structs
-
The attacks & preventions are:
-
Reentrancy attack:
-
Definition:
- One of the major dangers of calling external contracts is that they can take over the control flow. In the reentrancy attack (a.k.a. recursive call attack), a malicious contract calls back into the contract (callee) before the first invocation of the function is finished. This may cause the different invocations of the function to interact in undesirable ways.
- It can be problematic because calling external contracts passes control flow to them (caller). The contract (caller) may take over the control flow and keep calling the SC’s function in a recursive manner. E.g. draining the contract’s token balance via recursive call.
-
Prevention:
- Checks-Effects-Interactions pattern: the state changes has to be done prior to any callee to a contract call.
- ReentrancyGuard: apply a ReentrancyGuard as provided by OpenZeppelin, ensuring that all state changes are executed before making any external calls.
-
Examples:
// INSECURE mapping (address => uint) private userBalances; function withdrawBalance() public { uint amountToWithdraw = userBalances[msg.sender]; require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call withdrawBalance again userBalances[msg.sender] = 0; }
// SECURE mapping (address => uint) private userBalances; function withdrawBalance() public { uint amountToWithdraw = userBalances[msg.sender]; userBalances[msg.sender] = 0; require(msg.sender.call.value(amountToWithdraw)()); // The user's balance is already 0, so future invocations won't withdraw anything }
-
In this case, the attacker can call transfer() when their code is executed on the external call in withdrawBalance. Since their balance has not yet been set to 0, they are able to transfer the tokens even though they already received the withdrawal. This vulnerability was also used in the DAO attack.
// INSECURE mapping (address => uint256) private userBalances; function transfer(address to, uint256 amount) { if (userBalances[msg.sender] >= amount) { userBalances[to] += amount; userBalances[msg.sender] -= amount; } } function withdrawBalance() public { uint256 amountToWithdraw = userBalances[msg.sender]; require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call `transfer()` userBalances[msg.sender] = 0; }
// SECURE mapping (address => uint256) private userBalances; function transfer(address to, uint256 amount) { if (userBalances[msg.sender] >= amount) { userBalances[to] += amount; userBalances[msg.sender] -= amount; } } function withdrawBalance() public { uint256 amountToWithdraw = userBalances[msg.sender]; userBalances[msg.sender] = 0; require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call `transfer()` after the user balance state change }
-
Instead of writing 1st code snippet, 2nd code snippet is preferred. This vulnerability is detected by Slither.
// Inside a function ... vestingToken.transferFrom(msg.sender, address(this), _amount); emit TokenVested(_beneficiary, _amount, _unlockTimestamp, block.timestamp); ...
// Inside a function ... bool success = vestingToken.transferFrom(msg.sender, address(this), _amount); if(success) { emit TokenVested(_beneficiary, _amount, _unlockTimestamp, block.timestamp); } else { emit VestTransferFromFailed(_amount); revert("vestingToken.transferFrom function failed"); } ...
-
Resources:
- Watch this video
- Reentrancy by SWC
- Reentrancy by OpenZeppelin
-
-
Arithmetic Overflow/Underflow: Use SafeMath to prevent variable overflow in solidity compiler <
0.8.0
-
Input Sanitation: The constructor or function arguments' values have to be sanitized before applying to state changes.
-
Take the help of security tools to find smart contract vulnerabilities.
-
Take a look at the gas-reporter’s report generated (in form of table in terminal) during unit tests (run via CLI) using Hardhat tool, to prevent functions from failing due to touching gas limit.
-
Contracts' size have to be kept as small as possible to prevent the contract from exceeding the contract size limit i.e.
24.5 KB
. -
Arithmetic Operation has to be done carefully. A real-life exploit (happened in Yield V2) can be seen in the code shown below. Here, performed division between
scaledFYTokenCached
and_baseCached
before multiplication withtimeElapsed
. This introduced imprecise accuracy when calculating the cumulativeBalancesRatio.
Always check the arithmetic operations by altering the operands and operators using Foundry.
function _update( uint128 baseBalance, uint128 fyBalance, uint112 _baseCached, uint112 _fyTokenCached ) private { .... cumulativeBalancesRatio += (scaledFYTokenCached / _baseCached) * timeElapsed; .... }
-
Use these libraries for gas-optimized & secure contracts:
Use these tools for security analysis:
- Slither | Crytic
- MythX
- MythX CLI recommended than VSCode extension
- Solidity Scanner (for detecting precision loss in arithmetic operations)
- Echidna | Crytic (for fuzzing analysis)
- Use Openzeppelin libs like
Context.sol
- Debug
hardhat/console.sol
- Any inter-contract communication is to supported with
RenetrancyGuard.sol
- Try to minimize gas fees by reducing loop's use like "calculate total amount". Instead create a separate variable which is updated with adding of new parsed values to previous stored. Eg-1, Eg-2: Prezerv/Staking-contract
// struct definition
struct Price {
uint256 currentPrice; // current price at a timestamp
uint256 totalPrice; // total Price yet from 1st timestamp to till date
bool exist; // to check if the timestamp is valid
uint256 index; // index of timestamp/price. This is to get the total count
}
// mapping of token address & mapping of timestamp & Price struct
mapping( address => mapping(uint256 => Price) ) public mapTstampPrice;
// total price till date
uint256 public totalPrice;
// next available index
uint256 public availableIndex;
instead of
struct Price {
uint256 currentPrice; // current price at a timestamp
uint256 timestamp; // timestamp for current price
}
// mapping of token address & Price struct
mapping( address => Price ) public mapTstampPrice;
- Use verbose type naming for all (state variables, temp variables inside functions) variables.
- Use comments as much as possible & also follow NAT spec in Solidity documentation.
- Use documentation in
docs/
folder in this hierarchy:Home
Features
Implementation
Unit Testing
Deployment
Security Audit
- Use multiple checks to avoid for security.
- Don't use redundant event firing when transactions reverted i.e. Don't use this:
if(false) {
revert("failed due to transfer");
emit TransferFailed(_msgSender(), amountWei); // it can be put before `revert()`
}
List of errors/warnings to avoid: doc
- By Solidity Official
- By Consensys
- Common Bugs/Attacks and Best Practices
- To Sink Frontrunners, Send in the Submarines
- Ethereum is a Dark Forest
- A founder’s guide to smart contact audits
- SWC Registry
-
constant
replaced byview
in function -
msg.gas
replaced bygasleft()
in global variables -
now
replaced byblock.timestamp
in global variables -
send
(recipient.send(1 ether);
),transfer
(recipient.transfer(1 ether);
) is less safer than this:(bool success, ) = recipient.call{gas: 10000, value:1 ether}(new bytes(0)); require(success, "Transfer failed.");
- original discussion
- hence,
call
>transfer
>send
More
There are some dangers in using send: The transfer fails if the call stack depth is at 1024 (this can always be forced by the caller) and it also fails if the recipient runs out of gas. So in order to make safe Ether transfers, always check the return value of send, use transfer or even better: use a pattern where the recipient withdraws the money.
- The distinction between
address
andaddress payable
was introduced with version0.5.0
. More - a contract constructor can be defined by using the same name as the contract (say, "SimpleStorage"). This syntax has been deprecated as of Solidity version
0.5.0
and now the keyword constructor must be used. - Before version 0.8.0 enums could have more than 256 members and were represented by the smallest integer type just big enough to hold the value of any member. Now, it's represented by
uint8
type. This means 256 members is the max now. 0
is replaced byaddress(0)
like this:
require(_counters[account] != Counter(0)); // as per v0.5.17
require(_counters[account] != Counter(address(0))); // as per v0.8.6
callcode
is replaced withdelegatecall
. DELEGATECALL was a new opcode that was a bug fix for CALLCODE which did not preserve msg.sender and msg.value. If Alice invokes Bob who does DELEGATECALL to Charlie, the msg.sender in the DELEGATECALL is Alice (whereas if CALLCODE was used the msg.sender would be Bob). Reason.callcode
was until Homestead.
Checkout this fantastic tool to understand EVM Opcodes.
Total gas fees = Gas Units x Gas Price
GAS UNITS : Gas units is a number that depends on the amount of computation required for a transaction. As complexity of transaction (action(s)) ⬆, gas units ⬆.
E.g.
Sending network's native token: `21,000` gas units (min. units required for a txn)
Sending ERC20 token: `60,000` gas units
Minting NFT token: `120,000` gas units
GAS PRICE: Gas price is determined by the demand for making transactions. As the traffic ⬆, the gas price ⬆. The unit normally is Gwei
(also in Ether
, smallest: wei
).
E.g. If the Gas price is 100 Gwei
, then 1 gas unit costs 100 Gwei.
Hence, the total gas fees with Gas price assumed 100 Gwei:
Sending network's native token: 21,000 * 100 Gwei = 2,100,000 Gwei aka 0.0021 Ether
Sending ERC20 token: 60,000 * 100 Gwei = 6,000,000 Gwei aka 0.006 Ether
Minting NFT token: 120,000 * 100 Gwei = 12,000,000 Gwei aka 0.012 Ether
The price can be computed in USD. Assume 1 ETH = 1200 USD.
Sending network's native token: 21,000 * 100 Gwei = 2,100,000 Gwei aka 0.0021 Ether = 0.0021 * 1200 USD = 2.52 USD
Sending ERC20 token: 60,000 * 100 Gwei = 6,000,000 Gwei aka 0.006 Ether = 0.006 * 1200 = 7.2 USD
Minting NFT token: 120,000 * 100 Gwei = 12,000,000 Gwei aka 0.012 Ether = 0.012 * 1200 = 14.4 USD
- Payable: Unlike EOSIO, function can't be triggered by sending other tokens, but only ETH.
- Storage: Unlike EOSIO, there is no option to keep user's data onto their storage system. Because EOAs doesn't have any storage mechanism.
- Upgradeable: Contracts are not upgradeable which prevents a lot of customization after deployment. And it's dangerous as well. What if there is a bug. That's why SC Audit is a must. But, if the company doesn't have sufficient budget, as the price is hefty. For info, the SC Auditor's salary is min. 250 k USD annually.
Cross Chain Interoperability Protocol
Source: https://layerzero.network/
Source: https://axelar.network/
Coming soon...
Interaction with Smart contracts using binding languages like Javascript, Typescript, Python
- Using JS: Usually to interact with a smart contract on the Ethereum blockchain you use Web3js: you pass an ABI and an address, you call methods, and create transactions regarding the given smart contract.
- Using TS: Unfortunately, such dynamic interfaces (as above) — created during runtime — can’t be expressed in the Typescript type system.
- by default, there are these problems:
- No code completion
- There comes "Typechain".
- TypeChain is here to solve all these problems. It uses provided ABI files to generate typed wrappers for smart contracts. It still uses Web3js under the hood, but on the surface it provides robust, type safe API with support for promises and much more.
- TypeChain is a code generator - provide ABI file and name of your blockchain access library (ethers/truffle/web3.js) and you will get TypeScript typings compatible with a given library.
- Why TS over JS in Ethereum?
- Interacting with blockchain in Javascript is a pain. Developers need to remember not only a name of a given smart contract method or event but also it's full signature. This wastes time and might introduce bugs that will be triggered only in runtime. TypeChain solves these problems (as long as you use TypeScript).
- Installation
$ npm install --save-dev typechain
- For ethers:
$ npm install --save-dev @typechain/ethers-v5
(requires TS 4.0 >=) - For web3:
$ npm install --save-dev @typechain/web3-v1
- by default, there are these problems:
- Using Python: refer this
- In order to see any value inside Solidity just do this:
import "hardhat/console.sol";
// wherever needed inside the function
console.log("pre approved tokens");
console.log("print value: %s", v.d0);
- In order to see any value inside Typescript (inside test function) just do this:
// wherever needed inside the `describe`, `it` function
// console.log("pre approved tokens")
console.log("print value: %s", await lpToken.totalSupply())
Writing unit test functions for every
.skip()
: to skip a unit test function.only()
: to run a unit test functionbefore
,beforeEach
,afterEach
,after
Explained here- In case of reverting, use
await expect(fn())
instead ofexpect(await fn())
. BigNumber
in JS/TS is used to handle Solidity'suint256
basically more than 64-bit.- Try to use single return values function rather than multi-return values. It seems the multiple arrays and types seem to confuse the code causing this issue.
List of Warnings, Errors in Contract
- Cause: an struct inside a contract has same name as that of contract.
- Solution: Change struct or contract name. Rename them as different.
- Cause: only 24 KB size limit
- Solution: Use diamond standard
- Cause: only 24 KB size limit per facet
- Solution: reduce the error message i.e. the string inside
require()
statement. replace with custom error code like "CF0" instead of "ALREADY_ALLOCATED" & document the error codes.
4. of Member push not found or not visible after argument-dependent lookup in address payable[] storage ref
- Cause:
push
method is not available for dynamic array of typeaddress payable[]
. Another reason could be that the array is defined as fixed rather than dynamic in order to usepush
method. - Solution: just define w/o
payable
- Cause: The transaction would require a high gas limit to be set explicitly.
- Solution: Set a gas limit. If you set it too high, you will not be charged for unused gas, but it's still good practice to set a reasonable limit based on the complexity of the operation you're performing.
- Using
ethers-ts
:
- Using
const tx = await wTsscLzNova
.connect(signer1)
.setPeer(epId2, wTsscLzSepolia, { gasLimit: 8000000 });
const receipt = await tx.wait();
console.log(
`tx hash: \${tx.hash} in block #\${receipt.blockNumber}`
);
- Using
foundry
:
cast send $WTSSCLZ_NOVA "setPeer(uint32,bytes32)" $SEPOLIA_ENDPOINT_V2_ID 0x00000000000000000000000087Aca95Fb76D1617fCb068c4154594Ec6149b0fF --rpc-url $NOVA_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --gas-limit 1000000
Initially, tx failed with 46k gas consumption. So, decided to set a limit of 1M (safe side). So, the tx was successful. Tx url.
List of Warnings, Errors in Unit testing
- Cause: The signer is parsed.
- Solution: The address is parsed.
// Before
await token.mint(addr1, String(1e22));
// After
await token.mint(addr1.address, String(1e22));
- Cause: number >
1e18
parsed as number - Solution: number >
1e18
should be parsed as BigNumber
// Before
await token.mint(addr1.address, String(1e22));
// After
await token.mint(addr1.address, BigNumber.from("10000000000000000000000"));
- Cause: The object of which method is being called, has not been created yet.
- Solution: First create the object, & also ensure the variable if used in concatenated functions, then keep it as global.
-
Cause: This is because of using
await
insideexpect
. -
Solution: Instead use
await
beforeexpect
. -
Before:
expect(await stakingContract.getStakedAmtTot(ZERO_ADDRESS)).to.be.revertedWith(
"Invalid address"
);
- After
await expect(stakingContract.getStakedAmtTot(ZERO_ADDRESS)).to.be.revertedWith(
"Invalid address"
);
- Cause: The node has been upgraded. It’s likely that your application or a module you’re using is attempting to use an algorithm or key size which is no longer allowed by default with OpenSSL 3.0.
- Solution: Just downgrade the node back to the previous working version. Note: keep it > v14.0. Install via
sudo n v0.15.1
.
6. Error: overflow (fault="overflow", operation="toNumber", Error: invalid BigNumber value (argument="value", value=undefined, code=INVALID_ARGUMENT, version=bignumber/5.5.0)
- Cause: That number is too big to use .toNumber() on as it exceeds the 53-bits JavaScript IEEE754 number allows. It seems the multiple arrays and types seem to confuse the code causing this issue.
- Solution: Try to use single return function in a unit testing rather than multiple return values function like returning a struct.
// Before
// Here, the function `getUserRecord` returns multiple values
const [stakedAmtAfterUnstaking, , unstakedAmtAfterUnstaking, , rewardAmtAfterUnstaking] = await stakingContract.getUserRecord(token.address, addr1.address);
// After
// Here, the function `getUserRecord` returns single value
const rewardAmtAfterUnstaking = await stakingContract.getUserRewardAmt(token.address, addr1.address);
- Cause:
sub
is not defined forBigNumber
. - Solution: As
sub
is defined forBigNumber<promise>
, so addawait
to make the function being called aspromise
type.
Before:
// get the balance of addr2 before mint
const balanceAddr2Before: BigNumber =
erc20TokenContract.balanceOf(addr2.address);
...
...
// get the balance of addr2 after mint
const balanceAddr2After: BigNumber = erc20TokenContract.balanceOf(
addr2.address
);
await expect(balanceAddr2After.sub(balanceAddr2Before)).to.eq(
BigNumber.from(String(1))
);
After:
// get the balance of addr2 before mint
const balanceAddr2Before: BigNumber =
await erc20TokenContract.balanceOf(addr2.address);
...
...
// get the balance of addr2 after mint
const balanceAddr2After: BigNumber = await erc20TokenContract.balanceOf(
addr2.address
);
await expect(balanceAddr2After.sub(balanceAddr2Before)).to.eq(
BigNumber.from(String(1))
);
- Cause: using arithmetic operation on
String
. - Solution: All the
promise
based functions output intoString
. So, convert tonumber
usingparseInt
Before:
const depositedAmt = await vaultContract
.connect(addr1)
.getDepositedAmt();
// get the pUSD balance of addr1 after withdraw pUSD
const balance1Pre = await pusdCoinContract.balanceOf(addr1.address);
...
...
// get the pUSD balance of addr1 after withdraw pUSD
const balance1Post = await pusdCoinContract.balanceOf(addr1.address);
expect(balance1Post.sub(balance1Pre)).to.be.lessThan(depositedAmt);
After:
const depositedAmt = await vaultContract
.connect(addr1)
.getDepositedAmt();
// get the pUSD balance of addr1 after withdraw pUSD
const balance1Pre = await pusdCoinContract.balanceOf(addr1.address);
...
...
// get the pUSD balance of addr1 after withdraw pUSD
const balance1Post = await pusdCoinContract.balanceOf(addr1.address);
expect(parseInt(balance1Post.sub(balance1Pre))).to.be.lessThan(
parseInt(depositedAmt)
);
9. reason: 'cannot estimate gas; transaction may fail or may require manual gas limit' code: 'UNPREDICTABLE_GAS_LIMIT'
- Cause: There is some kind of ERC20 token being transferred to the contract using a function e.g.
allocatePC
for SC:crowdfunding-sc
. So, the caller doesn't have enough ERC20 token. Hence, it is throwing error. This happens in proxy based architecture including architecture like diamond standard, openzeppelin proxy pattern. - Solution: Mint PC to deployer & then interact with the contract's function -
allocatePC
NOTE:
allocatePC
function accepts ERC20 token & then set some activity based on requirement.
10. Error HH9: Error while loading Hardhat's configuration. You probably tried to import the "hardhat" module from your config or a file imported from it. This is not possible, as Hardhat can't be initialized while its config is being defined
- Cause: use of
require("hardhat")
insidehardhat.config.*
directly or indirectly. - Solution: It could be that this line is being used in deployment scripts in order to read hardhat raw values. So, it's beter to comment the line calling
require("hardhat")
indirectly into config file. Although we can deploy scripts otherwise.
- Cause: This happens when the contract variable is defined as
const
. - Solution: Just define as
let
Before:
const clipFactory: ContractFactory = await ethers.getContractFactory("Clip");
const clipContract: Contract = await clipFactory.deploy(
usdcTokenContract.address
);
await clipContract.deployed();
After:
const clipFactory: ContractFactory = await ethers.getContractFactory("Clip");
clipContract = await clipFactory.deploy(usdcTokenContract.address);
await clipContract.deployed();
declare
clipContract
inbeforeEach()
or outside.
- Deploy a contract at
addr1
and then upgrade the contract ataddr2
. Now,pause
the previous contract ataddr1
. - Cons:
- the storage variables data has to be moved from old contract to new contract address.
- the old contract address has to be replaced with new contract address wherever referenced. E.g. In case of Uniswap, the new version address is to be updated in the 3rd party's referenced contract addresses.
-
Deploy a contract at
addr1
and then upgrade the contract at the same addressaddr1
. -
DAO governance based control over the contract can be added on the top so as to prevent centralized decision-making about updating contracts.
-
Cons:
- the contract deployed is difficult for the security auditors as it is updateable all the time.
-
Proxy method is the most robust method to upgrade any contract. It is the 1st layer before interacting with the main contract. The contract's address is fed into the proxy contract.
- To update the contract, just change the
implementation
address in the proxy contract. And then the it will be routed to the new contract address.
-
To change the contract address, a DAO contract can be created inside the
upgrade
function and based on the voting result theimplementation
address would be allowed to change. -
In order to let your contracts get upgraded, create a proxy smart contract using OpenZeppelin by following this.
Watch this video.
- List of projects using Diamond standard: https://eip2535diamonds.substack.com/p/list-of-projects-using-eip-2535-diamonds
- The EIP-2535 Diamonds is a way to organize your Solidity code and contracts to give them the right amount of modularity and cohesion for your system. In addition, flexible upgrade capability is a key part of it.
- Diamond: A diamond is a contract that uses the external functions of other contracts as its own.
- Facet: The contracts that a diamond uses for its external functions.
- A diamond has a mapping which stores contract addresses corresponding to functions as key. When an external function is called on a diamond the diamond looks in the mapping to find the facet to retrieve the function from and execute.
mapping(bytes4 => address) facets;
In the diagram above you can see that functions func1
and func2
are associated with FacetA
. Functions func3
, func4
, func5
are associated with FacetB
. Functions func6
and func7
are associated with FacetC
.
Also in this diagram you see that different structs within the diamond are used by different facets. FacetA
uses DiamondStorage3
. FacetB
uses DiamondStorage3
and DiamondStorage2
. FacetC
uses DiamondStorage2
and DiamondStorage1
.
-
Storage: All the data is stored in the diamond storage in form of a
struct
, not in the facets. The facets contain only the functions, but can use multiple diamond storage. -
By default, when you create new state variables like unsigned integers, structs, mappings etc. Solidity automatically takes care of where exactly these things are stored within contract storage. But this default automatic functionality becomes a problem when upgrading diamonds with new facets. New facets declaring new state variables clobber existing state variables -- data for new state variables gets written to where existing state variables exist.
-
Diamond Storage solves this problem by bypassing Solidity's automatic storage location mechanism by enabling you to specify where your data gets stored within contract storage.
This might sound risky but it is not if you use a hash of a string that applies to your application or is specific to your application. Use the hash as the starting location of where to store your data in contract storage.
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
-
Doing that might seem risky to you too. But it is not. Realize that this is how Solidity's storage location mechanism works for maps and arrays. Solidity uses hashes of data for starting locations of data stored in contract storage. You can do it too.
-
Diamond storage: Since Solidity 0.6.4 it is possible to create pointers to structs in arbitrary places in contract storage. This enables diamonds and their facets to create their own storage layouts that are separate from each other and do not conflict with each other, but can still be shared between them.
-
Never explicitly define any state variable inside interface, library, facet, but only inside Diamond library which shall be put inside the Diamond proxy contract. The state variables shall be made available at Diamond storage position.
-
To interact with any Logic Contract, you interact with the Diamond Proxy Contract which in turn does 3 things
- It searches for where your function selector is stored and retrieves the facet address.
- After retrieving the facet address where that function selector is implemented, it performs a delegate call to that address in the context of * Diamond storage and returns any output to the caller(if any).
- To upgrade a diamond, you can either Add, Remove or Replace existing functions.
-
The Diamond templates all come with two pre-written facets that help to manage your diamond. Source
DiamondLoupeFacet: A Lopue is a magnifying glass used to inspect diamonds. This facet contains functions to help inspect the current state of your diamond, providing data about the current state of facets and function selectors.
DiamondCutFacet: A facet which allows you to make upgrade changes to your diamond.
-
Deploy sequence:To deploy a Diamond, you need to
- deploy the DiamodCutFacet then bind it to the Proxy Diamond contract(Diamond.sol).
- The diamond can now be upgraded with the other facets(DiamondcutFacet, DiamondToken and DiamondloupeFacet) using the DiamondcutFacet.
To make things easier, a script is available to do this. Reference
-
Understanding library usage in a diamond for facet:
- During deployment, the lib's internal functions that are used in a facet gets added into facet's (written with
contract
keyword) bytecode as well. Hence, the lib's internal functions are not independently deployed unlike its external functions counterpart in a library file. - The external functions defined in a library are not added to the facet's bytecode, rather has to be pre-deployed and then added to the diamond.
- When we flatten a facet, it considers the internal & external functions of the lib irrespective of its usage.
- During deployment, the lib's internal functions that are used in a facet gets added into facet's (written with
Idiots Guide to Using an EIP-2535 Diamond Proxy
EVM | Solana |
---|---|
Event | Event |
Modifier | Attach #[access_control()] attribute to a function |
Function | Function |
Variable, Array | Accounts in struct which store data |
public , private , external , internal |
pub , by default all are private |
Code, data stored in contract | Code, Data stored separately in different accounts. Each data account is owned by code/program account |
EOA, SCA | EOA, PDA, PA |
mapping | multiple derived PDAs |
For more, refer to Interview Q.s
- EOA: Externally Owned Account
- SCA: Smart Contract Account
- PDA: Program Derived Account
- PA: Program Account
- From Solidity to EOS contract development
- Learn Solidity | SC Programmer
- Solidity contract development specification
- Solidity Tutorial playlist
- Mappings in Solidity Explained in Under Two Minutes
- Gas Optimization in Solidity
- Gas Optimization in Solidity Part I: Variables
- Solidity: A Small Test of the Self-Destruct Operation
- The Curious Case of
_;
in Solidity - Ethernaut Solutions by CMichel
- How to Write Upgradable Smart Contracts
- EVM Opcodes
- Hitchhikers Guide to the EVM
- Upgrading your Smart Contracts | A Tutorial & Introduction
- Deploying More Efficient Upgradeable Contracts
- Understanding Diamonds on Ethereum
- What is Diamond Storage?
- EIP-2535
- How to Share Functions Between Facets of a Diamond
- Ethereum Diamonds Solve These Problems
- Aavegotchi follows Diamond standard
- Solidity Cheatsheet
- List of SC Auditing companies
- EVM written in Rust
- A collection of EVM opcodes puzzles that helps you learn in-depth details about EVM Opcodes by solving them
- An Ethereum Virtual Machine Opcodes Interactive Reference
- Ethereum Virtual Machine Opcodes