A cheatsheet for developing on the Ethereum platform
- Ethereum: A description of how Ethereum works.
- Dapp Architecture: A summary of Dapp architecture.
- Interacting with Contracts: Notes about ABIs
- Solidity Cheatsheet: Summary of common things in Solidity
- Web3 Cheatsheet: Summary of common things in Web3
- Truffle Cheatsheet: Summary of common things in Truffle
Ethereum is a blockchain that allows for executable code (called Contracts) to be put onto the blockchain and to be interacted with by all other accounts.
There are two types of accounts:
- Externally owned account (aka, wallet)
- Contract
Both types
- have a balance (in ETH)
- can transfer balance to other accounts
External accounts
- can send transactions to the blockchain
- transactions can simply transfer ETH
- or they can call contract functions
- or they can create contracts
- controlled by private keys
Contracts
- only ever do stuff via a transaction from an
external account
- can send messages to other contracts
- can have functions executed via transactions and messages
- do not have private keys (see first bullet)
All things on the blockchain get initiated by a transaction by an external account
. Everything that ever happens starts with external accounts
sending transactions to the blockchain.
When an external account
(sender) creates a transaction they specify the following:
- From: The account creating this transaction
- To: The target recipient of the transaction
- Data: If
To
is a contract, then which function to call and with what parameters - gasPrice: How much they are willing to pay (in wei) per one unit of gas
- gasLimit: How much total gas they are willing to pay for
- signature: Proving this transaction is from From
Note: the maximum transaction fee incurred will be gasPrice times gasLimit. The sender must have at least this balance
. Some other validations occur as well, such as ensuring the transaction has a proper signature, everything is encoded correctly, etc.
It is sent to the queue. A transaction hash
is created that represents this transaction.
Miners operate as follows:
- Look at all pending transactions
- Find the ones with the highest gasPrice (since they'll make the most)
- Find as many as can fit within the current block (there are gas and memory restrictions)
- Execute that transactions. For each one:
- as it gets executed, count how much gas is used. If it ever exceeds
gasLimit
stop immediately, store the tx as failed onto the blockchain. - execute code, update storage, put new contract data in block, do internal transactions, etc, etc.
- charge a transaction fee to
sender
(gasUsed times gasPrice) - add all relevant information into the block
- also add a
txReceipt
to the block
- as it gets executed, count how much gas is used. If it ever exceeds
- Start mining the block!
If the miner successfully mines, he broadcasts this to his peers. They will do the exact same process of executing all transactions to ensure the results are correct. If they accept the block, they start working on the next block.
This is the first confirmation
. Each additional block mined after this one is a confirmation.
At this point the txHash
is on the blockchain and should be contain all the info about what happened. How much gas was used, internal messages, etc.
I think "Dapp" is really a misnomer. What you have are three layers:
Smart contracts deployed onto the blockchain that do stuff and store data. These are usually written in Solidity, but can be written in anything that compiles to EVM
, a shitty, slow, super-limited assembly-like language obsessed with everything except usability.
A node is the middle-ware that connects the world to the blockchain. They expose the to the world via an RPC. So, you can do HTTP calls to them to get answers to some queries like:
- Look up any block
- Find balance of any address
- See what contracts are storing what
- Call
constant
contract functions (since they do not change state)
Under the hood, a node is part of the Ethereum network of miners and gets notified when new blocks are mined. A node contains the full blockchain.
When developing, you'll use TestRPC -- it's a fake node that pretends blocks are being mined and that contains its own blockchain. Very handy.
This is plain old JS that does HTTP calls to some Node, and uses the results to show you some UI about what is in the blockchain.
Depending on the browser and what extensions exist, the UI can include prompting the user to create a transaction with specific details, such as "call this contract function with these params passing this much wei with this gas limit and this gas price".
This will cause MetaMask and Mist to show a UI where the user can confirm the transaction.
When the frontend creates a transaction for metamask to confirm, MetaMask will show you NONE OF the following information:
- full address (or link to it on etherscan)
- contract function name
- function params
It's basically like "hey this website wanted to do this transaction which I won't tell you anything about. You cool with that?"
I haven't used Mist so I don't know if it's as stupid.
When contracts are deployed to the blockchain, they contain ZERO informatoin about how they can be interacted with. Basically, they are gigantic blobs of data, and you have to invoke them just right to get the results you want.
IMO this is a sadistic non-user-friendly approach, but I'm sure there are "good" reasons such as shaving a few KB per block or a few microseconds off mining time.
At any rate, in order to properly interact with contracts, one needs to know the ABI. The ABI includes all available functions (and their parameter types) so that valid calls can be made. Think of an ABI as a schema to what is inside the contract.
Under the hood, a client will convert what the user intends to do into the data
portion of a transaction. The full specs on this can be found in Ethereum docs somewhere... but you really don't want to know.
- Good luck with strings.
- No floating point. You never really do
a/b
. Instead, you do(someValue * a)/b
. It's fucking stupid. - External Transactions can not receive return values. Yes, seriously.
- Functions cannot return dynamic arrays.
In the below example, addr
is the recipient address.
bool _success = addr.send(valueInWei)
- Calls fallback function if
addr
is a contract - Returns boolean for success or not
- Sends small amount of gas (2300)
addr.transfer(valueInWei)
- Calls fallback function if
addr
is a contract - throws on failure
- Sends small amount of gas (2300)
bool _succees = addr.call.value(valueInWei).gas(uint)();
- Calls fallback function if
addr
is a contract - Returns boolean for success or not
- Sends custom amount of gas (defaults to 0: unlimited)
- Sends custom value
bytes4 _signature = bytes4(sha3("fnName(param1type,param2type)"))
bool _success = addr.call.value().gas()(_signature, param1, param2...)
- Returns true or false
- Sends custom amount of gas (defaults to 0: unlimited)
- Sends custom value
var _returnedValue = addr.someFunction(param1, param2,...);
- Returns whatever the function returns
- Sends unlimited gas
- Cannot set value
- !! Note: Solidity must know the type of
addr
and that it hassomeFunction
var _returnedValue = addr.someFunction.value(valueInWei).gas(uint)(param1, param2, ...);
- Returns whatever the function returns
- Sends custom amount of gas (defaults to 0: unlimited)
- Sends custom value
- !! Note: Solidity must know the type of
addr
and that it haspayable someFunction
It might be useful to know how to handle multiple return values.
Here:
var (return1, return2) = ...
Or if you know the types
(uint _re1, bytes32 _ret2) = ...
Truffle-contract is basically Web3, but returns promises. Not sure what else it does.
Most everything you deal with will be a transaction, and will in some way call sendTransaction
. Web3 docs can be found here.
Here's how to create transactions manually:
Example:
var options = {
// String - address this is from (and will be signed by)
from: "0xabc...",
// String - address of who its to
to: "0xdef...",
// Number|String|BigNumber - how much wei to send
value: 1000,
// Number|String|BigNumber - maximum gas to be used
gas: 1e10,
// Number|String|BigNumber - price of gas, defaults to mean network gas price
gasPrice: 22e12 // (22 gwei),
// String - optional byte string of contract call with params, or contract creation code
data: "0x3832...",
// Number - allows you to overwrite your own pending transactions
nonce: 123
}
var txHash = web3.eth.sendTransaction(options);
Web3 returns a txHash... I'm not sure if it's pre-mined or post-mined.
truffle-contract returns a promise fulfilled with an object:
{
tx: "0x123...",
receipt: {
transactionHash: '0x4b0cb3d24b374b27eb05dda5343e3435208c18171774afdd0c7147b9c80894cf',
transactionIndex: 0,
blockHash: '0x4513c0bb872b86f5c741d6b71f4373d7fda94ee4b865ce39423c94e29d03b3c5',
blockNumber: 2556,
gasUsed: 830927,
cumulativeGasUsed: 830927,
contractAddress: null,
logs: [ [Object], [Object], [Object] ]
}
// Only when called via a contract instance
// These will be logs specific to this object.
logs: [{ ...log1... }, { ...log2... }],
}
Note: It's not possible to get return values when doing a transaction, because Ethereum is so amazing.
**Note: Logs will contain matches from addresses other than the contract you are sending to if they match the topic name. This is a bug in web3. For example, if your contract has an event called "Foo" and other contracts called log a "Foo" event, all the "Foo"s will show up in the logs **
Web3 and truffle use ABIs to fill out some of the transaction params.
myContract.doStuff(arg1, arg2, options).then( ... )
Under the hood this will do a sendTransaction, but will fill out the to
, and data
.
No, Web3 and truffle-contract don't unfortunately use named params. And if you omit some arguments, things get really fucked up. So, be careful.
Doing a call is different than a TX. It does not touch the network -- no state will be changed. However, you can at least get a fucking return value back.
(Side note: I'm not sure what happens if you do if (addr.send()) { return 1; } else { return 0; }
inside of a call)
At any rate, it's about the same as the above, except tack on .call
and pass the params there. I don't know why you can't just tack on .asCall()
Official docs on filters. These docs are pretty good.
Todo: put code samples.
Official event docs here. These docs are pretty good.
You can watch for events on a specific object instance.
Use either instance.<EventName>(eventFilter, filterOpts)
or instance.allEvents(filterOps);
. They both return the same thing.
// a filter object. see above section for details
// note: I'm not sure what the default fromBlock is
var filterOpts = { ... };
// an object whose keys match the args of events
// and whose values will be used to filter
var eventFilter = {
arg1: "value must match this",
arg2: ["can be this", "or this"]
};
var eventFilter = null;
var watcherForEventName = myContract.EventName(eventFilter, filterOpts);
var watcherForAll = myContract.allEvents(filterOpts);
console.log(watcherForEventName.get());
/*
logs the following in truffle-contract:
[
{
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x36caf6b32b87a5145581df63d02fb02857d8935977721d324e8daadb6f2663f3',
blockHash: '0x6cec9e3a2ec6972291f71650758001e258f3f973b7fc10a0f81de24b468306f6',
blockNumber: 2168,
address: '0x8f7d11d92a76d10107f14f42142c4409e2e0a37c',
type: 'mined',
event: 'EventName',
args: {
arg1: <value>,
arg2: <value>
}
}, {...}
]
*/
- Using node8 async/await will make your life much more enjoyable, both in deploy and in tests.
module.exports = function(deployer, network, accounts) {
deployer.then(async function(){
console.log("Deploying first thing...");
await deployer.deploy(FirstContract);
firstContract = deployer.at(FirstContract.address);
console.log("Deploying the second thing...");
await deployer.deploy(SecondContract);
...
- If you edit a file
A.sol
that is imported by fileB.sol
,truffle compile
will not re-compileB.sol
. Just get in the habit of usingtruffle compile --all
. - If you experience a
solc
exception about5 5
, it's because there is a syntax error somewhere in any of your files. You're fucked. - If you are using my fork of
truffle-compile
you can dotruffle compile --parse
and you'll see any syntax errors on files about to be parsed. (This feature is currently a pull request)
- You cannot return a promise from your deployment function -- it is ignored. Truffle will consider a single migration completed as soon as
deployer
promise chain is finished. Consider putting everything inside a deployer.then():
// incorrect -- truffle will continue to next deployment because it thinks this is finished
module.exports = async function(deployer, network, accounts) {
await deployer.deploy(SomeContract);
// as soon as the last deployer.deploy() or .then() is done
// truffle thinks deployment is done. anything below will
// be run _after_... which can screw stuff up.
var c = SomeContract.at(SomeContract.address);
await c.doSomeCall();
// correct -- truffle will wait for this to finish before continuing
module.exports = function(deployer, network, accounts) {
// note: async function always returns a promise
deployer.then(async function(){
await deployer.deploy(SomeContract);
var c = SomeContract.at(SomeContract.address);
await c.doSomeCall();
deployer.deploy
bug: if you do not pass exact amount of constructors, it will pass none of them. It does not do validation on this, either.
// assume SomeContract takes 3 args
// incorrect - This creates SomeContract with empty args.
deployer.deploy(SomeContract, 5, 5);
// correct
deployer.deploy(SomeContract, 5, 5, 5);
deployer.deploy
returns a promise fulfilled with nothing... not even the address. To get the address of something deployed:
await deployer.deploy(SomeContract);
var c = SomeContract.at(SomeContract.address);
// or using promises
deployer.deploy(SomeContract).then(function(){
var c = SomeContract.at(SomeContract.address);
})
A lot of contracts you write might be time based, and so you may wish to "fast forward" testrpc to ensure your contracts work as designed. I was surprised how hard I had to search to find out how to do this.
Anyway, here's how to do it using web3:
function fastForward(timeInSeconds){
if (!Number.isInteger(timeInSeconds))
throw new Error("Passed a non-number: " + timeInSeconds);
if (timeInSeconds <= 0)
throw new Error("Can not fastforward a negative amount: " + timeInSeconds);
// move time forward.
web3.currentProvider.send({
jsonrpc: "2.0",
method: "evm_increaseTime",
params: [timeInSeconds],
id: new Date().getTime()
});
// mine a block to make sure future calls use updated time.
web3.currentProvider.send({
jsonrpc: "2.0",
method: "evm_mine",
params: null,
id: new Date().getTime()
});
}
I personally have a deployed Registry
contract that contains name=>address
mappings for singleton contracts. When my contracts need to talk to one another, they always ask the registry first.
When I need to upgrade a singleton, I deploy a new instance, and upgrade the registry name for it. Since all my contracts always ask the registry for the latest name, no further action should be necessary.
However, this only works because my singleton contracts do not store a lot of state, and when I deploy new versions I copy the state over.
If you require copying state over that is huge or unknown (eg, mappings), you will have to use the Relay
(aka delegatecall
) pattern.
For more on upgrading contracts, go here
When you throw
in Solidity, nothing is returned. No error message. No trace. Nothing. This is fucking stupid, and makes testing things a nightmare.
For example:
contract Foo {
function doStuff(){
require(msg.sender == "0xABC...");
require(msg.value > 100);
... other code ...
}
}
it("fails when wrong amount is sent", function(done){
myFoo.doStuff({from: "0xBCA...", 90})
.then(function(){ done("We expected this call to fail"); )
.catch(done);
});
This test will pass -- but for the wrong reason. It failed because you passed the wrong address. You never really tested that it fails because the wrong amount was sent.