Published
Edited
Sep 15, 2020
1 fork
Importers
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
defaultEnv = new Env
Insert cell
ganacheEnv = GanacheEnv()
Insert cell
GanacheEnv = async (provider, opts = {}) => {
return new Env({...opts, networkOrProvider: new Web3(provider || 'http://localhost:8545')})
}
Insert cell
function proxy(network, provider) {
if (provider && provider != true) {
if (typeof(provider) == 'string')
return new Web3.providers.HttpProvider(`${provider}/${network}`)
return provider;
}
return new Web3.providers.HttpProvider(`http://localhost:8546/${network}`)
}
Insert cell
MetaMaskEnv = async (opts = {}) => new Env({...opts, networkOrProvider: await MetaMaskProvider()})
Insert cell
async function MetaMaskProvider() {
return new Promise((okay, fail) => {
ethereum.sendAsync({method: 'eth_requestAccounts'}, error => error ? fail(error) : okay(ethereum))
})
}
Insert cell
Insert cell
Insert cell
Insert cell
DEFAULT_NETWORK = 'mainnet'
Insert cell
class Env {
constructor(opts = {}) {
this.abi = new ABI(this)
this.networkOrProvider = opts.networkOrProvider || DEFAULT_NETWORK;
this.createAccounts = opts.createAccounts || 1;
this.defaultFrom = opts.defaultFrom;
this.defaultGas = opts.defaultGas;
this.defaultGasPrice = opts.defaultGasPrice;
this.defaultValue = opts.defaultValue;

// serialized state
this.accounts = {} // address -> Account
this.contracts = {} // contract name -> Contract
this.instances = {} // instance name -> Instance
this.typedefs = {} // type name -> Typedef
this.variables = {} // var name -> any
this.logDecoders = {} // address -> function
this.createComplexTypes()
}

createComplexTypes() {
const env = this;

this.Enum = class Enum extends Function {
constructor(type, defn) {
super('...args', 'return this.__call__(...args)')
if (!Array.isArray(defn))
throw new Error(`expected array for enum '${type}' definition but got: ${str(defn)}`)
if (defn.length < 1)
throw new Error(`expected at least one member for enum '${type}'`)
const self = Object.assign(this, {...invert(defn, Number), type, defn, meta: 'enum'})
const bond = Object.assign(this.bind(this), self)
return env.define(type, bond)
}

__call__(name) {
const M = Math.max(Math.ceil(Math.log2(this.defn.length)), 8)
switch (realType(name)) {
case 'string':
if (!isFinite(name)) {
const i = this.defn.indexOf(name)
if (i < 0)
throw new Error(`enum '${this.type}' has no member '${name}'`)
return Object.assign(i, {abiType: `uint${M}`})
} // else fall through
case 'bigint':
case 'number':
if (name < 0 || name >= this.defn.length)
throw new Error(`enum '${this.type}' has no member '${name}'`)
return Object.assign(name, {abiType: `uint${M}`})
default:
throw new Error(`enum '${this.type}' cannot have member '${name}'`)
}
}
}

this.Struct = class Struct extends Function {
constructor(type, defn) {
super('...args', 'return this.__call__(...args)')
if (!isPojo(defn))
throw new Error(`expected object for enum '${type}' definition but got: ${str(defn)}`)
const self = Object.assign(this, {type, defn, meta: 'struct'})
const bond = Object.assign(this.bind(this), self)
return env.define(type, bond)
}

__call__(nameOrStruct) {
const struct = mapobj(this.defn, v => env.abi.cast(0, v))
switch (realType(nameOrStruct)) {
case 'string':
if (exists(nameOrStruct, this.variables))
return this(this.variables[nameOrStruct])
throw new Error(`struct '${nameOrStruct}' not defined in env`)
case 'object':
for (let field in nameOrStruct) {
if (!exists(field, this.defn))
throw new Error(`struct '${this.type}' has no field '${field}'`)
struct[field] = env.abi.cast(nameOrStruct[field], this.defn[field])
}
return Object.defineProperty(struct, 'abiType', {get: () => this.type})
case 'array':
const fields = Object.keys(this.defn), N = fields.length;
if (nameOrStruct.length != N)
throw new Error(`struct '${this.type}' has ${N} fields, got ${nameOrStruct.length}`)
for (let i = 0; i < N; i++)
struct[fields[i]] = env.abi.cast(nameOrStruct[i], this.defn[fields[i]])
return Object.defineProperty(struct, 'abiType', {get: () => this.type})
default:
if (nameOrStruct == 0) // special cases to return empty structs
return Object.defineProperty(struct, 'abiType', {get: () => this.type})
throw new Error(`could not cast ${str(nameOrStruct)} to struct '${this.type}'`)
}
}
}
}

define(name, type) {
return this.typedefs[name] = type;
}
async assign(data, force = false, kind = 'variables', overlay = false) {
await this._assign(data, this[kind], force, kind, overlay)
return this;
}
async _assign(data, into, force, kind = 'variables', overlay = false, path = []) {
for (const [key, maybePromise] of Object.entries(data)) {
const path_ = [kind, ...path.slice(1), key];
const val = await maybePromise;
if (overlay && isPojo(val)) {
const dst = into[key] = exists(key, into) ? into[key] : {}
if (isPojo(dst))
await this._assign(val, dst, force, kind, overlay, path_)
else
throw new Error(`tried to overlay object ${crumbs(path_)} -> ${str(val)} without force`)
} else if (overlay && Array.isArray(val)) {
const dst = into[key] = exists(key, into) ? into[key] : []
if (Array.isArray(dst))
await this._assign(val, dst, force, kind, overlay, path_)
else
throw new Error(`tried to overlay array ${crumbs(path_)} -> ${str(val)} without force`)
} else {
if (exists(key, into) && !areEqual(val, into[key]) && !force)
throw new Error(`tried to overwrite ${crumbs(path_)} -> ${str(val)} without force`)
else if (val === undefined)
delete into[key]
else
into[key] = val;
}
}
return into;
}
val(key, kind = 'variables') {
if (!exists(key, this[kind]))
throw new Error(`missing key in ${kind}: '${key}' not defined in env`)
return this[kind][key];
}
dumps() {
// Write a standard JSON conf that can be reloaded
return str({
accounts: this.accounts,
contracts: reject(this.contracts, k => k.startsWith('_')),
instances: reject(this.instances, k => k.startsWith('_')),
typedefs: mapobj(this.typedefs, t => ({type: t.type, defn: t.defn})),
variables: this.variables
})
}
async loads(str, force = false) {
// Load (back) an unparsed standard JSON conf
return this.load(JSON.parse(str), force)
}

async load(conf, force = false) {
// Try various methods of loading the conf
if (isPojo(conf))
(await this.loadContractsJson(conf, force) ||
await this.loadNetworksJson(conf, force) ||
await this.loadStandardJson(conf, force))
else
throw new Error(`unsupported conf format: ${conf}`)
return this;
}
async loadContractsJson(json, force) {
if (json.contracts && json.version) {
const contracts = {}
for (let [key, val] of Object.entries(json.contracts)) {
const [path, name] = key.split(':')
if (!name || !val || !val.abi || !val.metadata)
return false;
const contract = contracts[name] = {
name,
abi: JSON.parse(val.abi),
bin: val.bin,
metadata: JSON.parse(val.metadata)
}
await this.indexLogs(name, contract.abi)
}
await this.assign(contracts, force, 'contracts', true)
return true;
}
}
async loadNetworksJson(json, force) {
if (json.Blocks && json.Constructors && json.Contracts) {
const instances = {}
for (let [name, address] of Object.entries(json.Contracts))
instances[name] = Object.assign(instances[name] || {}, {name, address})
for (let [name, rawArgs] of Object.entries(json.Constructors))
instances[name] = Object.assign(instances[name] || {}, {name, rawArgs})
for (let [name, deployBlock] of Object.entries(json.Blocks))
instances[name] = Object.assign(instances[name] || {}, {name, deployBlock})
await this.assign(instances, force, 'instances', true)
await this.assign(json.Contracts, force, 'variables')
return true;
}
}
async loadStandardJson(json, force) {
for (let kind of ['accounts', 'contracts', 'instances', 'variables']) {
if (json[kind])
await this.assign(json[kind], force, kind)
}
if (json.typedefs)
this.loadTypedefs(json.typedefs, force)
return true;
}
async loadTypedefs(typedefs, force) {
// TODO: when !force, check for overwriting typedefs?
for (let {meta, type, defn} of Object.values(typedefs)) {
switch (meta) {
case 'enum': new (this.Enum)(type, defn); break;
case 'struct': new (this.Struct)(type, defn); break;
default:
throw new Error(`unrecognized meta type '${meta}' for '${type}'`)
}
}
}

async read(url) {
return this.load(await (await fetch(url)).json())
}

async fetchContractABI(contractNameOrAddress, network = DEFAULT_NETWORK, remember = true) {
// Find the ABI if possible, and possibly remember it along the way
if (isAddressLike(contractNameOrAddress)) {
const address = contractNameOrAddress;
const instance = Object.values(this.instances).find(i => i.address == address)
if (instance && typeof instance.contractABI == 'object') // includes 'null'
return instance.contractABI;
let contractABI = null; // cached after the first fetch, no matter what
try {
contractABI = await etherscan.getABI(address, network)
} catch (e) {}
if (remember) {
if (instance) {
instance.contractABI = contractABI;
} else {
this.instances[address] = {name: address, address, contractABI}
}
}
return contractABI;
} else {
const contractName = contractNameOrAddress;
const contract = this.contracts[contractName]
if (contract)
return contract.abi;
}
}
async decodeLogs(logs, network = DEFAULT_NETWORK) {
const instanceAddrs = Object.values(this.instances).map(i => i.address)
const contractNames = Object.keys(this.contracts)
const fallbacks = instanceAddrs.concat(contractNames)
const decoded = []
for (let log of logs) // not parallelized on purpose to avoid DoSing ourselves
decoded.push(await this.decodeLog(log, network, log.address, fallbacks))
return decoded;
}
async decodeLog(log, network, contractNameOrAddress, fallbacks = []) {
let contractDecoders = this.logDecoders[contractNameOrAddress]
if (!contractDecoders) {
const contractABI = await this.fetchContractABI(contractNameOrAddress, network)
if (contractABI) {
// index all the log decoders by the primary topic
contractDecoders = await this.indexLogs(contractNameOrAddress, contractABI)
} else if (fallbacks.length) {
// no abi - move on
return this.decodeLog(log, network, fallbacks[0], fallbacks.slice(1))
} else {
// no abi - nowehere else to look
return {'$ABI_NOT_FOUND': log}
}
}

// try to decode (using the primary topic)
const decoder = contractDecoders[log.topics[0]]
if (decoder) {
return decoder(log)
} else if (fallbacks.length) {
return this.decodeLog(log, network, fallbacks[0], fallbacks.slice(1))
} else {
return {'$EVENT_NOT_FOUND': log}
}
}
async indexLogs(contractNameOrAddress, contractABI) {
return contractABI.filter(e => e.type == 'event').reduce((acc, event) => {
if (event.anonymous) {
console.warn(`not indexing anonymous event`, event)
} else {
const mainTopic = this.keccak(`${event.name}(${event.inputs.map(i => i.type).join(',')})`)
acc[mainTopic] = this.abi.logDecoder(event)
}
return acc;
}, this.logDecoders[contractNameOrAddress] = {})
}
keccak(string) {
return (new Web3).utils.keccak256(string)
}
address(nameOrAddress) {
// Find addr for name or address: string | throw
switch (realType(nameOrAddress)) {
case 'bigint':
case 'number':
return `0x${nameOrAddress.toString(16).padStart(40, '0')}`.toLowerCase()
case 'string':
if (isAddressLike(nameOrAddress))
return nameOrAddress.toLowerCase()
if (exists(nameOrAddress, this.accounts))
return this.address(this.accounts[nameOrAddress].address)
if (exists(nameOrAddress, this.instances))
return this.address(this.instances[nameOrAddress].address)
if (exists(nameOrAddress, this.variables))
return this.address(this.variables[nameOrAddress])
default:
throw new Error(`address '${nameOrAddress}' not defined in env`)
}
}
array(typeOrTypeName) {
const typedArray = (arrayNameOrArray) => {
// Find struct array with name or array of typeName: [<typeName>] | throw
let typeName = typeOrTypeName;
if (typeName instanceof Function)
typeName = this.abi.resolve(typeName).type;
switch (realType(arrayNameOrArray)) {
case 'array':
return Object.assign(arrayNameOrArray.slice(), {abiType: `${typeName}[]`})
case 'bigint':
case 'number':
return Object.assign(Array(arrayNameOrArray), {abiType: `${typeName}[]`})
case 'string':
default:
if (exists(arrayNameOrArray, this.variables))
return typedArray(this.variables[arrayNameOrArray])
throw new Error(`array ${arrayNameOrArray} not defined in env`)
}
}
return typedArray;
}

bool(nameOrBoolean) {
// Find bool for name or boolean: boolean | throw
switch (realType(nameOrBoolean)) {
case 'string':
if (exists(nameOrBoolean, this.variables))
return this.bool(this.variables[nameOrBoolean])
throw new Error(`bool '${nameOrBoolean}' not defined in env`)
default:
return Object.assign(Boolean(nameOrBoolean), {abiType: 'bool'})
}
}
bytes(data, M = '') {
switch (realType(data)) {
case 'bigint':
case 'number':
return this.bytes(`0x${data.toString(16)}`, M)
case 'string':
const b = (data.length - 2) / 2, B = 2 * M + 2;
if (!data.startsWith('0x'))
throw new Error(`bytes${M} '${data}' does not start with 0x`)
else if (M && b > B)
throw new Error(`bytes${M} '${data}' has ${b} > ${M} bytes`)
return Object.assign(String(data.padEnd(B, '0')), {abiType: `bytes${M}`})
default:
throw new Error(`bytes${M} cannot convert from ${realType(data)} '${data}'`)
}
}
string(data) {
return Object.assign(data, {abiType: 'string'})
}

int(nameOrNumber, M = 256) {
return Object.assign(this.uint(nameOrNumber), {abiType: `int${M}`})
}

uint(nameOrNumber, M = 256) {
// Find uint for name or number: bigint | throw
const tag = n => Object.assign(n, {abiType: `uint${M}`})
switch (realType(nameOrNumber)) {
case 'bigint': return tag(nameOrNumber)
case 'number': return tag(BigInt(Math.floor(nameOrNumber)))
case 'string':
if (nameOrNumber.match(/^(0x[a-fA-F0-9]*|\d*)$/))
return this.uint(BigInt(nameOrNumber), M)
if (exists(nameOrNumber, this.variables))
return this.uint(this.variables[nameOrNumber], M)
throw new Error(`uint '${nameOrNumber}' not defined in env`)
default:
throw new Error(`uint '${nameOrNumber}' not recognized`)
}
}
custom(typeName) {
if (exists(typeName, this.typedefs))
return this.typedefs[typeName];
throw new Error(`custom type '${typeName}' not defined in env`)
}
bindings() {
return {
abi: this.abi,
keccak: this.keccak,
val: this.val.bind(this),
address: this.address.bind(this),
array: this.array.bind(this),
bool: this.bool.bind(this),
bytes: this.bytes.bind(this),
string: this.string.bind(this),
int: this.int.bind(this),
uint: this.uint.bind(this),
Enum: this.Enum,
Struct: this.Struct,
...this.typedefs
}
}
async library(lib, vars) {
// Convenience to create a library with properly scoped bindings
const bindings = this.bindings()
return {...bindings, ...await lib(bindings, this, vars)}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class World {
constructor(env = defaultEnv) {
this.env = env;
this.web3 = w3i(env.networkOrProvider)
this.cache = {}
this.history = [];
this.createdAccounts = this.createAccounts(env.createAccounts)
this.loadedAccounts = this.loadAccounts() // NB: async
this.monkeyedWeb3 = this.monkeyWeb3() // NB: async
}
createAccounts(n, entropy) {
// Create accounts locally, in the wallet (probably don't have ether)
const accounts = this.web3.eth.accounts.wallet.create(n, entropy)
const aliases = {}
for (let i = 0; i < n; i++)
aliases[`@${i}`] = select(accounts[i], ['address', 'privateKey'])
this.env.assign(aliases, true, 'accounts')
return accounts;
}
async loadAccounts() {
// Load accounts remotely, on the node (hopefully have some ether)
const accounts = await this.web3.eth.getAccounts()
const aliases = {}
for (let i = 0; i < accounts.length; i++)
aliases[`$${i}`] = {address: accounts[i]}
this.env.assign(aliases, true, 'accounts')
return accounts;
}
async monkeyWeb3() {
if (/ethereumjs/i.test(await this.web3.eth.getNodeInfo())) {
this.ethereumjs = true;
this.web3.eth.transactionConfirmationBlocks = 1;
this.web3.eth.transactionBlockTimeout = 5;
}
return this.web3;
}
async network(readCache = true) {
if (readCache && this.cache.network)
return this.cache.network;

const web3 = this.web3;
const network = await web3.eth.net.getNetworkType(web3.currentProvider)
return this.cache.network = (() => {
switch (network) {
case 'main':
return 'mainnet'
case 'private':
const match = web3.currentProvider.host.match(/(\w*?)(-eth.compound.finance|\.infura\.io)/)
if (match)
return match[1]
default:
return network;
}
})();
}
async contractCode(addr) {
const {address} = this.env.bindings()
return this.rpc('eth_getCode', {params: [address(addr)]})
}
async lastBlock(readCache = true) {
if (readCache && this.cache.lastBlock)
return this.cache.lastBlock;
return this.cache.lastBlock = await this.web3.eth.getBlock("latest")
}

async gasLimit(readCache = true) {
const block = await this.lastBlock(readCache)
return block ? block.gasLimit : 1e6;
}

async txOpts(given) {
const opts = await resolve(given)
const fallbackFrom = async () => {
const nodeAccounts = await this.loadedAccounts;
return nodeAccounts.length ? '$0' : '@0'
}
return {
from: this.env.address(await unthunk(dfn(opts.from, this.env.defaultFrom, fallbackFrom))),
gas: await unthunk(dfn(opts.gas, this.env.defaultGas, Math.floor(await this.gasLimit() * 0.9))),
gasPrice: await unthunk(dfn(opts.gasPrice, this.env.defaultGasPrice, 0)),
value: (await unthunk(dfn(opts.value, this.env.defaultValue, 0))).toString()
}
}
async txReceipt(txHash) {
return this.rpc('eth_getTransactionReceipt', {params: [txHash]})
}
async deploy(contractOrContractName, opts = {}, acc) {
// Deploy a contract
// {args?, emits?, expect?, revert?, as?, force?, recycle?, ...txOpts?}

// Just for logging
const network = await this.network(), tag = `${network}${this.ethereumjs ? ' fork' : ''}`
let contract;
if (contractOrContractName.bin) {
contract = contractOrContractName;
} else if (!(contract = this.env.contracts[contractOrContractName])) {
throw new Error(`contract not found: '${contractOrContractName}' has no bytecode available`)
}
if (opts.recycle && exists(opts.as, this.env.instances)) {
const instance = this.env.instances[opts.as]
const chainBin = await this.contractCode(opts.as)
if (chainBin != '0x') {
// TODO: should recycled deploys be in history? should we fetch logs, etc.?
console.log(`[${tag}] Recyling deploy for '${contract.name}, found '${opts.as}'`, instance)
return instance;
}
}
const step = {deploy: contract.name, ...opts}
const args = step.args = await unthunk(step.args || [], acc)
const sendOptions = await this.txOpts(opts)
const rawArgs = this.env.abi.encode(args)
const data = `0x${contract.bin}${rawArgs.slice(2)}`
const tx = Object.assign(sendOptions, {data})
const deployed = this.web3.eth.sendTransaction(tx)
const pending = new Promise((ok, err) => deployed.on('receipt', ok).on('error', err))
const deployTx = await this.maybeRevert(pending, step)

let result, logs;
if (deployTx.matchedError) {
console.log(`[${tag}] Successfully matched error on deploying '${contract.name}'`, deployTx)
result = deployTx;
} else {
console.log(`[${tag}] Deployed '${contract.name}', awaiting receipt`, deployTx, args)
const deployTxReceipt = await this.txReceipt(deployTx.transactionHash)
console.log(`[${tag}] Received receipt for '${contract.name}'`, deployTxReceipt)
result = {
name: step.as,
address: deployTxReceipt.contractAddress,
rawArgs,
deployBlock: deployTxReceipt.blockNumber,
deployTx,
deployTxReceipt,
contractABI: contract.abi,
contractBin: contract.bin,
contractName: contract.name
}
logs = await this.maybeEmits(deployTxReceipt, opts)
}

await this.maybeExpect(result, opts)
await this.maybeSaveInEnv(step.as, result, step.force, 'instances')
this.history.push({...step, result, logs})
return result;
}
async send(method, opts = {}, acc) {
// Send a transaction
// {to, args?, emits?, expect?, revert?, assign?, force?, ...txOpts?}
const step = {send: method, ...opts}
const to = await assert(this.env.address(step.to), `must specify contract 'to' to send '${method}'`)
const args = step.args = await unthunk(step.args || [], acc)
const sendOptions = await this.txOpts(step)
const data = this.env.abi.encodeFunctionCall(method, args)
const tx = Object.assign(sendOptions, {to, data})
const sent = this.web3.eth.sendTransaction(tx)
const pending = new Promise((ok, err) => sent.on('receipt', ok).on('error', err))
const result = await this.maybeRevert(pending, step)

// Just for logging
const network = await this.network(), tag = `${network}${this.ethereumjs ? ' fork' : ''}`
console.log(`[${tag}] Sent '${method}' to '${step.to}'`, args, result)

const logs = await this.maybeEmits(result, step)
await this.maybeExpect(result, step)
await this.maybeSaveInEnv(step.assign, result, step.force)
this.history.push({...step, result, logs})
return result;
}
async call(method, opts = {}, acc) {
// Call a function without modifying the chain
// {on, args?, returns?, at?, expect?, revert?, assign?, force?, ...txOpts?}
const step = {call: method, ...opts}
const to = await assert(this.env.address(step.on), `must specify contract 'on' to call '${method}'`)
const args = step.args = await unthunk(step.args || [], acc)
const returns = step.returns ? (x => this.env.abi.decodeOne(x, step.returns)) : (x => x)
const callOptions = await this.txOpts(opts)
const data = this.env.abi.encodeFunctionCall(method, args)
const tx = Object.assign(callOptions, {to, data})
const block = await step.at;
const pending = this.web3.eth.call(tx, block)
const result = await returns(await this.maybeRevert(pending, step))
await this.maybeExpect(result, opts)
await this.maybeSaveInEnv(step.assign, result, step.force)
this.history.push({...step, result})
return result;
}

async rpc(method, opts = {}, acc) {
// Make a jsonrpc to the provider
// {params?, returns?, expect?, assign?, force?}
const step = {rpc: method, ...opts}
const params = step.params = await unthunk(step.params || [], acc)
const returns = step.returns || (x => x)
const provider = this.web3.currentProvider
const response = await promise(done => provider.send({method, params, jsonrpc: '2.0', id: 0}, done))
if (response.error)
throw new Error(`rpc error: ${response.error.message}`)
const result = await returns(response.result)
await this.maybeExpect(result, step)
await this.maybeSaveInEnv(step.assign, result, step.force)
this.history.push({...step, result})
return result;
}
async eval(fn, opts = {}, acc) {
// Just evaluate a fn, with the ability to save/expect
// {expect?, assign?, force?}
const step = {eval: fn, ...opts}
const result = fn(acc, step)
await this.maybeExpect(result, step)
await this.maybeSaveInEnv(step.assign, result, step.force)
this.history.push({...step, result})
return result;
}
async exec(steps, acc) {
// Perform a sequence of instructions
if (Array.isArray(steps)) {
for (let step of steps)
acc = await this.exec(step, acc);
return acc;
} else if (steps) {
const step = steps;
const ops = ['deploy', 'send', 'call', 'rpc', 'eval'], numOps = countKeys(step, ops)
if (step instanceof Function) {
return await step(this, acc)
} else if (numOps == 0) {
throw new Error(`bad instruction: ${str(step)} has none of ${str(ops)}`)
} else if (numOps > 1) {
throw new Error(`bad instruction: ${str(step)} has more than one of ${str(ops)}`)
} else {
try {
if (step.deploy) return await this.deploy(step.deploy, step, acc)
else if (step.send) return await this.send(step.send, step, acc)
else if (step.call) return await this.call(step.call, step, acc)
else if (step.rpc) return await this.rpc(step.rpc, step, acc)
else if (step.eval) return await this.eval(step.eval, step, acc)
} catch (e) {
const summary = this.summarizeStep(step)
console.error(`unexpected failure in ${summary}: ${e.message}`, e, step)
throw e;
}
}
} else {
throw new Error(`bad instruction: exec cannot process step, got '${steps}'`)
}
}
async fork(network, params) {
// Another way of calling, just for convenience
return fork(network, params, this)
}
async emits(sub, emits) {
// Exec the sub then check that the logs produced match the emits spec
const i = this.history.length;
await this.exec(sub)
const logs = this.history.slice(i).reduce((a, e) => a.concat(e.logs || []), [])
return this.checkEmits(logs, emits)
}
async invariant(ac, b, d = 0) {
// Assert an invariant holds before and after some events
const prior = await this.exec(ac)
await this.exec(b)
const post = await this.exec(ac)
if (d instanceof Function) {
if (!d(prior, post))
throw new Error(`invariant broken: (${str(prior)}, ${str(post)}) failed ${d}`)
} else if (post - prior != d) {
throw new Error(`invariant broken: ${str(post)} - ${str(prior)} == ${prior - post} != ${d}`)
}
return {prior, post}
}
async reverts(sub, revert) {
// Exec the sub and check for reverts
const pending = this.exec(sub)
return this.maybeRevert(pending, {revert})
}
async tail(contractNameOrAddress, topics = [], opts = {}) {
// Get past logs for an address
const network = await this.network()
const address = this.env.address(contractNameOrAddress)
let fromBlock, toBlock;
if (opts.blocks != undefined) {
const last = await this.lastBlock(false)
fromBlock = last.number - opts.blocks;
toBlock = last.number;
} else {
fromBlock = dfn(opts.fromBlock, 'earliest')
toBlock = dfn(opts.toBlock, 'latest')
}
if (!Array.isArray(topics)) {
const contractABI = await this.env.fetchContractABI(address, network)
const eventTypes = (contractABI || []).reduce((acc, abi) => {
if (abi.type == 'event') {
const sig = `${abi.name}(${abi.inputs.map(i => i.type).join(',')})`;
const tps = acc[abi.name] = acc[abi.name] || [];
tps.push(this.env.keccak(sig))
}
return acc;
}, {})
topics = [eventTypes[topics]];
}
const logs = await this.web3.eth.getPastLogs({address, fromBlock, toBlock, topics})
return this.env.decodeLogs(logs, network)
}

abiEqual(a, b) {
return areEqual(this.env.abi.strip(a), this.env.abi.strip(b))
}

revertMessage(err) {
const match = err.message.match(
/^.*"?Returned error: VM Exception while processing transaction: revert\s*(.*?)"?$/
)
return match && match[1]
}
summarizeStep(step) {
if (step.deploy) return `{deploy: '${step.deploy}' as: '${step.as}' args: ${str(step.args)}}`
else if (step.send) return `{send: '${step.send}' to: '${step.to}' args: ${str(step.args)}}`
else if (step.call) return `{call: '${step.call}' on: '${step.on}' args: ${str(step.args)}}`
else if (step.rpc) return `{rpc: '${step.rpc}' params: ${str(step.params)}}`
else if (step.eval) return `{eval: '${step.eval}'}`
throw new Error(`not a step: ${str(step)}`)
}
async checkEmits(logs, emits, ctx) {
if (emits instanceof Function) {
if (await emits(logs, ctx) !== true)
throw new Error(`unmet emission: ${str(logs)} failed ${emits}`)
} else {
const name = log => Object.keys(log)[0]
let i, remaining = logs;
for (const expected of [].concat(emits)) {
if (typeof expected == 'string' && expected.startsWith('!')) { // check *all*, not just remaining
const unwanted = logs.find(l => name(l) == expected.substr(1))
if (unwanted)
throw new Error(`unexpected log: ${str(expected)} but got ${str(unwanted)}`)
} else {
let found = false
for (i = 0; i < remaining.length; i++) {
const actual = remaining[i];
if (typeof expected == 'string') {
if (expected == name(actual)) {
found = true;
break;
}
} else {
if (name(expected) == name(actual)) {
if (!this.abiEqual(expected, actual))
throw new Error(`bad log match: ${str(expected)} != ${str(actual)}`)
found = true;
break;
}
}
}

if (found) {
remaining = remaining.slice(i + 1)
} else {
throw new Error(`unmatched log: ${str(expected)} not in ordered ${str(logs)}`)
}
}
}
}
return logs;
}

async maybeRevert(pending, step) {
// Revert can *only* match the exact revert message
// but expect can be used in conjunction!
let result;
try {
result = await pending;
} catch (error) {
if (exists('revert', step)) {
const revert = this.revertMessage(error)
if (revert != step.revert) {
throw new Error(`expected revert b/c "${step.revert}" but got "${revert}"`)
}
result = {step, matchedError: error}
} else {
throw new Error(`unexpected error "${error.message}"`)
}
}
return result;
}

async maybeEmits(receipt, step) {
// Emits can be a value or a function (*not* a thunk)
// same as expect, fns are require to return exactly 'true'
const logs = await this.env.decodeLogs(receipt.logs || [], await this.network())
if (exists('emits', step)) {
const emits = await step.emits;
await this.checkEmits(logs, emits, receipt)
}
return logs;
}

async maybeExpect(obj, step) {
// Expect can be a value or a function (*not* a thunk)
// but fns required to return exactly 'true' to protect against thunking accidents
if (exists('expect', step)) {
const expect = await step.expect;
if (expect instanceof Function) {
if (await expect(obj) !== true) {
throw new Error(`unmet expectation: ${str(obj)} failed ${expect}`)
}
} else if (!this.abiEqual(obj, expect)) {
throw new Error(`unmet expectation: ${str(obj)} != ${str(expect)}`)
}
}
}
async maybeSaveInEnv(key, val, force, kind = 'variables') {
if (key)
await this.env.assign({[key]: val}, force, kind)
}
}
Insert cell
Insert cell
Insert cell
Insert cell
class Maybe {
constructor(just) {
this.just = just;
}
}
Insert cell
function maybe(x) {
return new Maybe(x)
}
Insert cell
function all(X, pred = (x) => x) {
return X.every(pred)
}
Insert cell
function dfn(value, fallback, ...rest) {
if (value === undefined)
return rest.length == 0 ? fallback : dfn(fallback, ...rest)
return value;
}
Insert cell
function str(o) {
return JSON.stringify(o, (k, v) => realType(v) === 'bigint' ? v.toString() : v)
}
Insert cell
function zip(A, B) {
return A.map((a, i) => [a, B[i]])
}
Insert cell
function crumbs(p) {
return `|${p.join('.')}|`
}
Insert cell
function exists(k, o) {
return o.hasOwnProperty(k)
}
Insert cell
function isAddressLike(s) {
return s.match(/0x[a-fA-F0-9]{40}/)
}
Insert cell
function isPojo(o) {
return (o === null || typeof o !== 'object') ? false : Object.getPrototypeOf(o) === Object.prototype
}
Insert cell
function realType(o) {
if (Array.isArray(o)) return 'array'
if (o instanceof Function) return 'function'
if (o instanceof Boolean) return 'boolean'
if (o instanceof BigInt) return 'bigint'
if (o instanceof Number) return 'number'
if (o instanceof String) return 'string'
return typeof o;
}
Insert cell
function areEqual(A, B) {
if (Array.isArray(A))
return Array.isArray(B) && A.length == B.length ? all(zip(A, B).map(([a, b]) => areEqual(a, b))) : false;

if (typeof A == 'object') {
if (!typeof B == 'object') return false;
for (let k in A) if (!areEqual(A[k], B[k])) return false;
for (let k in B) if (!areEqual(B[k], A[k])) return false;
return true;
}

return A == B;
}
Insert cell
function countKeys(object, keys) {
let count = 0;
for (const key of keys)
count += exists(key, object)
return count;
}
Insert cell
function invert(object, fn = (x) => x) {
const inverted = {}
for (const key in object)
inverted[object[key]] = fn(key)
return inverted;
}
Insert cell
function filter(object, pred = () => true) {
const view = {}
for (const key in object)
if (pred(key, object))
view[key] = object[key];
return view;
}
Insert cell
function reject(object, pred = () => false) {
return filter(object, (...args) => !pred(...args))
}
Insert cell
function select(object, keys) {
const view = {}
for (const key of keys)
if (exists(key, object))
view[key] = object[key];
return view;
}
Insert cell
function where(object, pred = (k, v) => v !== undefined) {
const view = {}
for (const [key, val] of Object.entries(object))
if (pred(key, val))
view[key] = val;
return view;
}
Insert cell
function mapobj(object, fn = (v) => v) {
const view = {}
for (const [key, val] of Object.entries(object))
view[key] = fn(val, key, object)
return view;
}
Insert cell
async function promise(fn) {
return new Promise((okay, fail) => fn((err, res) => err ? fail(err) : okay(res)))
}
Insert cell
async function resolve(object) {
const copy = {}
for (const key in object)
copy[key] = await object[key];
return copy;
}
Insert cell
async function unthunk(v, a, depth = 1) {
if (Array.isArray(v) && depth > 0)
return Promise.all(v.map(x => unthunk(x, a, depth - 1)))
return v instanceof Function ? v(a) : v;
}
Insert cell
async function assert(test, reason = `failed assertion: ${test}`) {
const pass = await unthunk(test)
if (pass)
return pass;
throw new Error(reason)
}
Insert cell
function approx(v, V, p = 1, q = 100) {
return (V > v ? V - v : v - V) <= p * V / q;
}
Insert cell
function curry(fn, ...args) {
return fn.bind(fn, ...args)
}
Insert cell
function pipe(v, f) {
if (v instanceof Promise)
return v.then(f)
return f(v)
}
Insert cell
Insert cell
Insert cell
class ABI {
constructor(env) {
this.env = env;
this.coder = (new Web3).eth.abi;
}
decode(data, shape, strip = true) {
// The shape passed in can refer to custom types
// use the resolved shape to decode, the unresolved one to cast
if (shape instanceof Maybe)
try { return this.decode(data, shape.just, strip) } catch (e) { return }
const arr = (o) => Array(o.__length__).fill(0).map((_, i) => o[i])
const raw = this.coder.decodeParameters(this.resolveTop(shape), data)
const dec = this.cast(arr(raw), shape)
return strip ? pipe(dec, v => this.strip(v)) : dec;
}
decodeOne(data, shape, strip = true) {
if (shape instanceof Maybe)
try { return this.decodeOne(data, shape.just, strip) } catch (e) { return }
const raw = this.coder.decodeParameter(this.resolve(shape), data)
const dec = this.cast(raw, shape)
return strip ? pipe(dec, v => this.strip(v)) : dec;
}
encode(values) {
// The shapes we generate are always in terms of primitives
return this.coder.encodeParameters(this.resolveTop(this.shape(values)), this.strip(values))
}

encodeOne(value) {
return this.coder.encodeParameter(this.resolve(this.shape(value)), this.strip(value))
}

encodePacked(value) {
throw new Error(`TODO: implement encodePacked - does any other lib actually have it?`)
}

encodeConstructorArgs(contractABI, args) {
const constructorABI = contractABI.find((x) => x.type === 'constructor')
return constructorABI ? this.coder.encodeParameters(constructorABI.inputs, this.strip(args)) : '0x'
}

encodeFunctionCall(method, args) {
return this.coder.encodeFunctionCall({name: method, inputs: this.resolveTop(this.shape(args).components)}, this.strip(args))
}
logDecoder(event) {
return (log) => {
const dec = this.coder.decodeLog(event.inputs, log.data, log.topics.slice(1))
return {[event.name]: where(dec, k => !(isFinite(k) || k == '__length__'))}
}
}

shape(value, name = null) {
// Determine the type envelope of the value, for encoding
// does not fully resolve to primitive abi types

// First check for an explict abiType
if (value.abiType)
return {type: value.abiType, name}

// Then deal with Arrays, which are *tuples*, which are anonymous structs
if (Array.isArray(value)) {
return {type: 'tuple', components: value.map((v, i) => this.shape(v, i)), name}
}

// String('')-s like this are always bytes
if (value instanceof String)
return {type: 'bytes', name}

// Finally try the primitive types we recognize
switch (typeof value) {
case 'boolean': return {type: 'bool', name}
case 'bigint':
case 'number':
return {type: 'uint256', name}
case 'string':
// ''-strings which look like addrs are addrs
// otherwise let them be strings
if (isAddressLike(value))
return {type: 'address', name}
return {type: 'string', name}
}

throw new Error(`indeterminate abi type for ${str(value)}`)
}
cast(data, shape) {
// Cast the data according to the shape, for decoding
// data is assumed to already match the envelope
if (Array.isArray(shape)) {
return shape.map((s, i) => this.cast(data[i], s))
} else if (shape instanceof Function) {
return shape(data)
} else if (typeof shape == 'string') {
if (shape.match(/\[\d*\]$/)) {
return this.cast(data, this.env.array(shape.slice(0, -2)))
} else if (shape == 'address') {
return this.env.address(data)
} else if (shape == 'bool') {
return this.env.bool(data)
} else if (shape.startsWith('int')) {
return this.env.int(data, shape.slice(3) || 256)
} else if (shape.startsWith('uint')) {
return this.env.uint(data, shape.slice(4) || 256)
} else if (shape.startsWith('fixed')) {
throw new Error(`TODO: implement fixed<M>x<N> (${shape})`)
} else if (shape.startsWith('ufixed')) {
throw new Error(`TODO: implement ufixed<M>x<N> (${shape})`)
} else if (shape.startsWith('bytes')) {
return this.env.bytes(data, shape.slice(5))
} else if (shape == 'string') {
return data;
} else {
const typedef = this.env.custom(shape)
switch (typedef.meta) {
case 'enum':
return typedef(data)
case 'struct':
default:
return typedef(mapobj(data, (v, k) => this.cast(v, shape[k])))
}
}
} else if (shape.type) {
if (shape.type == 'tuple') {
return data.map((d, i) => this.cast(d, shape.components[i]))
} else if (shape.type == 'tuple[]') {
return data.map((d, i) => this.cast(d, {...shape, type: 'tuple'}))
} else {
return this.cast(data, shape.type)
}
} else if (typeof shape == 'object') {
const components = Object.values(shape)
if (isPojo(data)) {
return this.cast(Object.keys(shape).map(k => data[k]), shape)
} else if (Array.isArray(data)) {
return data.map((d, i) => this.cast(d, components[i]))
} else {
return components.map(c => this.cast(data, c))
}
} else {
console.error(`unrecognized shape`, shape, data)
throw new Error(`unrecognized shape: ${shape}`)
}
}

resolveTop(shape) {
const resolved = this.resolve(shape)
return resolved.components || resolved;
}

resolve(shape, name = null) {
// Fully resolves the shape to an abi with primitive types
if (Array.isArray(shape)) {
return {type: 'tuple', components: shape.map((s, i) => this.resolve(s, i)), name}
} else if (shape instanceof Function) {
return this.resolve(this.shape(shape(0)), name || shape.name)
} else if (shape instanceof Maybe) {
return this.resolve(shape.just, name)
} else if (typeof shape == 'string') {
return this.resolveStr(shape, name)
} else if (shape.type) {
return this.resolveABI(shape, name || shape.name)
} else {
const fields = mapobj(shape, (v, k) => this.resolve(v, k))
return {type: 'tuple', components: Object.values(fields), name}
}
}
resolveABI(shape, name = shape.name) {
// Resolve an ABI to its primitive types
const typeInfo = this.typeInfo(shape.type)

if (typeInfo.tuple)
return {...shape, components: shape.components.map(c => this.resolveABI(c)), name}

if (typeInfo.custom) {
// custom types get all their information from just the type name
return this.resolveStr(shape.type, shape.name)
}

return {...shape, name}
}

resolveStr(shape, name) {
// Resolve a 'string' shape to an ABI with primitive types
const typeInfo = this.typeInfo(shape)

if (typeInfo.tuple)
throw new Error(`missing components for 'tuple': supply the abi or use a custom type`)

if (typeInfo.array) {
if (typeInfo.basic) {
return {type: shape, name}
} else if (typeInfo.custom) {
const typedef = this.env.custom(typeInfo.custom.baseType)
switch (typedef.meta) {
case 'enum':
return {type: `${typedef(0).abiType}[]`, name}
case 'struct':
default:
const fields = mapobj(typedef.defn, (v, k) => this.resolve(v, k))
return {type: 'tuple[]', components: Object.values(fields), name}
}
} else {
throw new Error(`unrecognized shape '${shape}' with type info: ${str(typeInfo)}`)
}
}

if (typeInfo.basic)
return {type: shape, name}

if (typeInfo.custom) {
const typedef = this.env.custom(typeInfo.custom.baseType)
switch (typedef.meta) {
case 'enum':
return {type: typedef(0).abiType, name}
case 'struct':
const fields = mapobj(typedef.defn, (v, k) => this.resolve(v, k))
return {type: 'tuple', components: Object.values(fields), name}
}
}

throw new Error(`unrecognized shape '${shape}' (should not be possible)`)
}

typeInfo(str) {
// abi types:
// array: *[]
// basic: address* | bool* | int* | uint* | fixed* | ufixed* | bytes* | string*
// tuple: tuple | tuple[]
// custom: !basic && !tuple
// primitive: !custom (i.e. basic || tuple)
// basic, tuple, and custom types MAY also be arrays
const patterns = {
array: /^(?<baseType>.*)\[\]$/,
basic: /^(?<baseType>address|bool|int|uint|fixed|ufixed|bytes|string).*$/,
tuple: /^(?<baseType>tuple).*$/,
custom: /^(?<baseType>.*?)(\[\])?$/
}
const groups = mapobj(mapobj(patterns, p => str.match(p)), m => m && m.groups)
return {...groups, custom: !groups.basic && !groups.tuple && groups.custom}
}
strip(value) {
// Strip away any boxing we did to preserve the abi type
if (Array.isArray(value))
return value.map(v => this.strip(v))
if (value instanceof Boolean)
return Boolean(value.valueOf())
if (value instanceof BigInt)
return BigInt(value.valueOf())
if (value instanceof Number)
return Number(value.valueOf())
if (value instanceof String)
return String(value.valueOf())
if (typeof value == 'object')
return mapobj(value, v => this.strip(v))
return value;
}
}
Insert cell
Insert cell
Insert cell
DEFAULT_ETHERSCAN_API_KEY = 'K6KM4HKJ5DDH3FSUU9KYAPJWGFV4H1UAGY' // imports should override me
Insert cell
etherscan = ({
link: (network, address) => {
const host = network == 'mainnet' ? 'etherscan.io' : `${network}.etherscan.io`
return `https://${host}/address/${address}`
},

endpoint: (network) => {
const subdomain = network == 'mainnet' ? 'api' : `api-${network}`
return `https://${subdomain}.etherscan.io/api`
},

getABI: async function (address, network = DEFAULT_NETWORK, api_key = DEFAULT_ETHERSCAN_API_KEY) {
const json = await (await fetch(`${this.endpoint(network)}?module=contract&action=getabi&address=${address}&apikey=${api_key}`)).json()
if (json.status == 0)
throw new Error(json.result)
return JSON.parse(json.result)
},
getSource: async function (address, network = DEFAULT_NETWORK, api_key = DEFAULT_ETHERSCAN_API_KEY) {
const json = await (await fetch(`${this.endpoint(network)}?module=contract&action=getsourcecode&address=${address}&apikey=${api_key}`)).json()
return json.result;
}
})
Insert cell
Insert cell
Insert cell
Web3 = require('https://cdn.jsdelivr.net/gh/ethereum/web3.js@1.2.11/dist/web3.min.js')
Insert cell
function w3i(networkOrProvider) {
if (typeof networkOrProvider == 'string' && networkOrProvider.match(/^\w+$/))
return new Web3(new Web3.providers.HttpProvider(`https://${networkOrProvider}-eth.compound.finance`))
return new Web3(networkOrProvider)
}
Insert cell
Insert cell
Insert cell
function exp(fraction, digits = 18, capture = 6) {
// Convert a fraction to an exponential, e.g. [fraction]e[digits]
// `fraction` keeps `capture` decimals before upscaling
// large numbers can get chopped if too large
return BigInt(Math.floor(fraction * 10**capture)) * 10n**BigInt(digits - capture)
}
Insert cell
num = {
function num(numOrPromise, decimals = 1) {
return pipe(numOrPromise, n => n / 10**decimals)
}

return Object.assign(num, {
wei: n => num(n, 18),
hex: n => pipe(n, BigInt)
})
}
Insert cell
Insert cell
Insert cell
Insert cell
DEFAULT_GITHUB_REPO = 'compound-finance/compound-protocol'
Insert cell
DEFAULT_GITHUB_PATH = `networks/${DEFAULT_NETWORK}.json`
Insert cell
DEFAULT_GITHUB_BRANCH = 'master'
Insert cell
function github(path = DEFAULT_GITHUB_PATH, repo = DEFAULT_GITHUB_REPO, branch = DEFAULT_GITHUB_BRANCH) {
return `https://raw.githubusercontent.com/${repo}/${branch}/${path}`
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function EVMLib({uint}) {
const N = x => Number(uint(x))
return {
$blockNumber: () => ({rpc: 'eth_blockNumber', returns: Number}),
$mineBlock: () => ({rpc: 'evm_mine'}),
$mineBlockNumber: (blockNumber) => ({rpc: 'evm_mineBlockNumber', params: [N(blockNumber)]}),
$minerStart: () => ({rpc: 'miner_start'}),
$minerStop: () => ({rpc: 'miner_stop'}),
$setTime: (date) => ({rpc: 'evm_setTime', params: [date]}), // note: ganache code looks broken
$increaseTime: (seconds) => ({rpc: 'evm_increaseTime', params: [N(seconds)]}),
$freezeTime: (seconds) => ({rpc: 'evm_freezeTime', params: [N(seconds)]}),
$unfreezeTime: (seconds) => ({rpc: 'evm_unfreezeTime', params: [N(seconds)]}),
$$advanceBlocks: (blocks) => [
{rpc: 'eth_blockNumber'},
{rpc: 'evm_mineBlockNumber', params: B => [Number(B) + N(blocks) - 1]}
]
}
}
Insert cell
Insert cell
Insert cell
function ERC20Lib({address}) {
return {
$name: (token) => ({call: 'name', on: token, returns: 'string'}),
$symbol: (token) => ({call: 'symbol', on: token, returns: 'string'}),
$decimals: (token) => ({call: 'decimals', on: token, returns: Number}),
$totalSupply: (token) => ({call: 'totalSupply', on: token, returns: BigInt}),
$balanceOf: (token, owner) => ({call: 'balanceOf', on: token, args: [address(owner)], returns: BigInt}),
$transfer: (token, to, value) => ({send: 'transfer', to: token, args: [address(to), value]}),
$transferFrom: (token, from, to, value) => ({send: 'transferFrom', to: token, args: [address(from), address(to), value]}),
$approve: (token, spender, amount) => ({send: 'approve', to: token, args: [address(spender), amount]}),
$allowance: (token, owner, spender) => ({call: 'allowance', on: token, args: [address(owner), address(spender)], returns: BigInt})
}
}
Insert cell
Insert cell
Insert cell
async function fork(networkOrProvider, params, world) {
const {blockNumber, useProxy, unlocked_accounts, ...rest} = params || {};
const base = w3i(useProxy ? proxy(networkOrProvider, useProxy) : networkOrProvider)
const baseLastBlock = await base.eth.getBlock("latest")
const options = await world.rpc('evm_reset', {params: [
{
allowUnlimitedContractSize: true,
fork: base.currentProvider.host + (blockNumber ? `@${blockNumber}` : ''),
gasLimit: await baseLastBlock.gasLimit, // maintain configured gas limit
gasPrice: '20000',
unlocked_accounts: (unlocked_accounts || []).map(a => world.env.address(a)),
...rest
}
]})
const accounts = await world.loadAccounts()
return {accounts, options}
}
Insert cell
Insert cell
async function _runTests() {
const tests = [
async function simpleEvalTest() {
const acc = await (new World).exec([{eval: a => a + 1, expect: 2}], 1)
const result1 = await (new World).eval(a => a + 1, {expect: 2}, 1)
const result2 = await (new World).eval(a => a + 1, {expect: v => v == 2}, 1)
await assert(result1 == result2)
return {acc, result1, result2}
},
async function evalAccumulateTest(world) {
await assert(await world.exec([{eval: a => a + 1}, {eval: a => a * 2}], 7) == 16)
return {world}
},
async function evmLibTest(world, {uint}) {
const {
$blockNumber,
$setTime,
$increaseTime,
$$advanceBlocks
} = await world.env.library(EVMLib)

await world.env.assign({blocks: 333})
const block1 = await world.exec($blockNumber())
await world.exec($$advanceBlocks(+100))
const block2 = await world.exec($blockNumber())
await assert(block2 == (block1 + 100))
await world.exec($$advanceBlocks(-100))
const block3 = await world.exec($blockNumber())
await assert(block3 == (block2 - 100))
await world.exec($$advanceBlocks('blocks'))
const block4 = await world.exec($blockNumber())
await assert(block4 == (block3 + 333))
const offset = await world.exec($increaseTime(1111))
await assert(Number.isInteger(offset))

return {block1, block2, block3, block4}
},

async function etherscanTest(world, {address}) {
const env = await world.env.read(github())
const abi = await etherscan.getABI(address('Comptroller'))
const src = await etherscan.getSource(address('cBAT'))
const clone = await (new Env).loads(env.dumps())
await assert(areEqual(env.dumps(), clone.dumps()))
return {env, abi, src, clone}
},

async function basicTest(world, {val, address, uint, bool}) {
const a = address(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc) // careful!
const b = address(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dcn) // tricky
const c = address('0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc')
await assert(a != b)
await assert(b == c)
await assert(c == '0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc')
await assert(approx(99, 100) && !approx(98, 100) && !approx(99, 101))
await assert(approx(1, 2, 50) && !approx(1, 2, 49))
await assert(approx(0, 0))
await assert(approx(3n, 4n, 25n, 100n) && !approx(3n, 4n, 24n, 100n))

const _ = await world.env.assign({key: 10000000, key2: '0x1b', key3: '27'})
const value = val('key')
const unsigned = uint('key')
await assert(value == 10000000)
await assert(unsigned == 10000000n)
await assert(uint('key2') == 27)
await assert(uint('key3') == 27)
await assert(areEqual(unsigned, uint(uint(value))))

const accounts = await world.loadedAccounts;
const coinbaseInt = await num.hex(world.rpc('eth_coinbase'))
const lastBlock = await world.rpc('eth_getBlockByNumber', {params: ['latest', true]})
const nopCall = await world.call('notAMethod', {on: 0xabc, args: [3, 'ok', address(0)]})
const badSend = await world.send('notAMethod', {to: 123, args: [uint(4), bool(false)]})
return {a, b, c, coinbaseInt, lastBlock, nopCall, badSend}
},
async function forkAndLoadTest(world, {address}) {
const network = 'mainnet'
const env = await world.env.read(github(`networks/${network}.json`))
const forkInfo = await fork(network, {unlocked_accounts: [address('Timelock')]}, world)
const firstBal = await num.wei(world.web3.eth.getBalance(address('$0')))
const badSend = await world.send('_setBorrowPaused', {
to: address('Comptroller'),
args: [address('cBAT'), true],
revert: 'only pause guardian and admin can pause'
})
const badSend2 = await world.reverts(
{send: '_setBorrowPaused', to: 'Comptroller', args: [address('cBAT'), true]},
'only pause guardian and admin can pause'
)
return {forkInfo, firstBal, badSend, badSend2}
},
async function forkBlockTest(world) {
const network = 'mainnet', blockNumber = 10000000;
const env = await world.env.read(github(`networks/${network}.json`))
const forkInfo = await fork(network, {blockNumber}, world)
const lastBlock = await world.lastBlock(false)
await assert(lastBlock.number == blockNumber + 1)
return {env, forkInfo, lastBlock}
},
async function tailTest() {
const world = new World(new Env)
const env = await world.env.read(github(`networks/mainnet.json`))
const tail = await world.tail('cDAI', 'Mint', {blocks: 1000})
await assert(Object.keys(tail[0]) == 'Mint')
const tail2 = await world.tail('PriceData', 'Write', {blocks: 10000})
await assert(Object.keys(tail2[0]) == 'Write')
return {world, env, tail, tail2}
},
async function abiEncodeFunctionCallTest(world, {abi, address, uint, bool}) {
const traditional = abi.coder.encodeFunctionCall({
name: 'myMethod',
type: 'function',
inputs: [
{type: 'uint256', name: 'myNumber'},
{type: 'string', name: 'myString'}
]
}, ['2345675643', 'Hello!%'])

const simplified = abi.encodeFunctionCall('myMethod', [2345675643, 'Hello!%'])
await assert(traditional == simplified, 'abi encoding broken!')

const envelope = abi.shape([uint(3), bool(false), address(123)])
await assert(areEqual(envelope.components.map(c => c.type), ['uint256', 'bool', 'address']))

return {traditional, simplified, envelope}
},
async function abiEncodeDecodeTest(world, {abi, address, array, string, uint}) {
const i = [address(0), uint(0), array(string)(["welcome to my world"])]
const s = abi.shape(i)
const e = abi.encode(i)
const d = abi.decode(e, s)
await assert(areEqual(d, i))
return {i, s, e, d}
},
async function abiArrayTest(world, {abi, address, array, bytes, uint}) {
await assert(uint(2, 8).abiType == 'uint8')
await assert(abi.shape(array('string')(['s'])).type == 'string[]')
await assert(abi.shape(array('MyType')([{}])).type == 'MyType[]')

const e1a = abi.coder.encodeParameters(['uint8[]','bytes32'], [['34','255'], '0x324567fff'])
const e1b = abi.encode([array('uint8')(['34', '255']), bytes('0x324567fff', 32)])
await assert(e1a == e1b, 'abi encoding broken')

const e2a = abi.coder.encodeParameters(['uint8[]'], [['34','255']])
const e2b = abi.encode([array('uint8')(['34', '255'])])
await assert(e2a == e2b, 'abi encoding broken')

const e3a = abi.coder.encodeParameter('uint8[]', ['34','255'])
const e3b = abi.encodeOne(array('uint8')([34, 255n]))
await assert(e3a == e3b, 'abi encoding broken')
const z4 = [
array('address')([]),
array('bytes')([abi.encode([address(123)])])
]
const e4b = abi.encode(z4)
const d4b = abi.decode(e4b, abi.shape(z4))
await assert(areEqual(d4b, z4))

return {e1a, e1b, e2a, e2b, e3a, e3b, z4, e4b, d4b}
},

async function abiStructTest(world, {abi, array, uint, Struct}) {
const ParentShape = {
p1: 'uint256',
p2: 'uint256',
Child: {
p1: 'uint256',
p2: 'uint256'
}
}

const Parent = new Struct('Parent', ParentShape) // define type

const ParentABI = {
type: 'tuple',
components: [
{type: 'uint256', name: 'p1'},
{type: 'uint256', name: 'p2',},
{type: 'tuple',
name: 'Child',
components: [
{type: 'uint256', name: 'p1'},
{type: 'uint256', name: 'p2'}]}]}
await assert(areEqual(abi.resolve(abi.shape(Parent({}))), ParentABI), 'abi shape resolve broken')

const classic = abi.coder.encodeParameters(
['uint8[]', {Parent: ParentShape}],
[['34','255'], {p1: '42', p2: '56', Child: {p1: '45', p2: '78'}}]
)

const modern = abi.encode([
array('uint8')([34, '255']),
Parent({p1: 42, p2: 56, Child: {p1: '45', p2: 78n}})
])

await assert(classic == modern, `abi encoding broken`)

const expect = [[34, 255n], ['42', '56', ['45', '78']]]
const dc = abi.coder.decodeParameters(['uint8[]', {Parent: ParentShape}], classic)
const dm = abi.decode(modern, ['uint8[]', ParentABI])
await assert(!areEqual(dc, dm), `should be slightly different structures`)
await assert(areEqual(dm, expect), `abi decoding broken`)
const Observation = new Struct('Observation', {
timestamp: 'uint256',
acc: 'uint256'
})
const data = ["1595543220","73553348765004598352779970224"]
const cast = abi.cast(data, Observation)
await assert(areEqual(abi.strip(cast), {timestamp: 1595543220n, acc: 73553348765004598352779970224n}))

return {classic, modern, dc, dm, cast}
},
async function abiStructTest2(world, {abi, address, array, bytes, Enum, Struct}) {
const PriceSource = new Enum('PriceSource', ['FIXED_ETH', 'FIXED_USD', 'REPORTER'])
const TokenConfig = new Struct('TokenConfig', {
cToken: 'address',
underlying: 'address',
symbolHash: 'bytes32',
baseUnit: 'uint256',
priceSource: 'PriceSource',
fixedPrice: 'uint256',
uniswapMarket: 'address',
isUniswapReversed: 'bool'
})
const {PriceSource: p, TokenConfig: t} = world.env.bindings()
await assert(p === PriceSource)
await assert(t === TokenConfig)

const t0 = TokenConfig(0)
const t1 = TokenConfig({symbolHash: bytes('0x324567fff', 32)})
const e1 = abi.encodeOne(t1)
const d1 = abi.decodeOne(e1, TokenConfig)
await assert(areEqual(d1, abi.strip(t1)))

// TODO: deploy(bin, {args})
/*
await world.deploy(contractBin, {as: 'abiStructTest2', args: [
address(0),
address(1),
exp(.2),
1800,
array('TokenConfig')([])
]})
*/
return {PriceSource, TokenConfig, t0, e1, d1}
},
async function abiBytesTest(world, {abi, array, bytes, keccak}) {
const e4a = abi.coder.encodeParameters(['bytes32'], ['0x324567fff'])
const e4b = abi.encode([bytes('0x324567fff', 32)])
const e4c = abi.encode([bytes(0x324567fff, 32)])
await assert(e4a == e4b)
await assert(e4a == e4c)

const e5a = abi.coder.encodeParameters(['bytes16'], ['0x32456000000000000000000000000000'])
const e5b = abi.encode([bytes('0x32456000000000000000000000000000', 16)])
const e5c = abi.encode([bytes('0x32456', 16)])
const e5d = abi.encode([bytes(0x32456, 16)])
await assert(e5a == e5b)
await assert(e5a == e5c)
await assert(e5a == e5c)

const e6a = abi.coder.encodeParameters(['bytes32'], ['0x324567fff'])
const e6b = abi.encode([bytes('0x324567fff', 32)])
await assert(e6a == e6b)
await assert(keccak('abc') == '0x4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45')

return {abi, bytes, e4a, e4b, e4c, e5a, e5b, e5c, e5d, e6a, e6b}
},
async function abiMaybeTest(world, {abi, address}) {
const a = maybe(address)
const s = maybe('string')

const d1 = abi.decode('0x', a)
const d2 = abi.decode('0x', s)
await assert(d1 === undefined)
await assert(d2 === undefined)
const a3 = address(123)
const d3 = abi.decodeOne(abi.encodeOne(a3), a)
await assert(d3 == a3)
const s4 = 'hello'
const d4 = abi.decode(abi.encode([s4]), [s])
await assert(d4 == s4)
return {a, s, d1, d2, a3, d3, s4, d4}
},

async function zeroTypesTest(world, {address, array, bool, bytes, int, uint, Enum, Struct}) {
const anEnum = new Enum('anEnum', ['price'])
const aStruct = new Struct('aStruct', {})
return {
address: address(0),
array_address: array(address)(0),
array_uint: array('uint')(0),
bool: bool(0),
bytes: bytes(0),
int: int(0),
uint: uint(0),
enum: anEnum(0),
struct: aStruct(0)
}
},
async function fullForkTest(world, {array, address, bytes, string, uint}) {
const network = 'mainnet', blockNumber = 10517750, unlocked_accounts = ['BAT'];
const env = await world.env.read(github(`networks/${network}.json`))
const forkInfo = await world.fork(network, {blockNumber, unlocked_accounts, useProxy: true})
const result = await world.exec([
{call: 'enterMarkets', on: 'Comptroller', args: [array(address)([address('cETH')])], returns: array(uint), from: '0xF977814e90dA44bFA03b6295A0616a897441aceC'},
{send: 'propose', to: 'GovernorAlpha', args: [
array(address)([]),
array(uint)([]),
array(string)([]),
array(bytes)([]),
string("proposal description")
], revert: "GovernorAlpha::propose: proposer votes below proposal threshold"},
{rpc: 'eth_getBlockByNumber', params: ['latest', true], expect: b => b.number == 10517752},
{send: 'approve', to: 'BAT', from: 'BAT', args: [address('cBAT'), exp(10000)], gasPrice: 0},
{call: 'balanceOf', on: 'cBAT', args: [address('BAT')], returns: uint, expect: 0},
{send: 'mint', to: 'cBAT', from: 'BAT', args: [exp(100)], gasPrice: 0, emits: ['Mint', '!Failure']},
{call: 'balanceOf', on: 'cBAT', args: [address('BAT')], returns: uint, expect: 490735421750},
{call: 'balanceOf', on: 'SAI', args: [address('cSAI')], returns: num.wei, expect: x => ~~x == 341006},
{send: 'redeem', to: 'cBAT', from: 'BAT', args: [exp(100)], gasPrice: 0, emits: 'Failure'}
])
const logs = await world.emits([
{send: 'approve', to: 'BAT', from: 'BAT', args: [address('cBAT'), exp(10000)]},
{send: 'redeem', to: 'cBAT', from: 'BAT', args: [exp(100)]}
], {Failure: {error: 9, info: 46 , detail: 3}})

const {prior, post} = await world.invariant(
{call: 'balanceOf', on: 'BAT', args: [address('BAT')], returns: uint},
{send: 'mint', to: 'cBAT', from: 'BAT', args: [exp(100)]},
exp(-100))
const batBin = await world.contractCode('cBAT')
const nilBin = await world.contractCode(address(0))
await assert(batBin && batBin != nilBin)
await assert(nilBin == "0x")

return {world, result, logs, prior, post, batBin, nilBin}
},
async function deployTest(world, {address}) {
const tiny = {
name: 'Tiny',
bin: '608060405234801561001057600080fd5b5060405161016b38038061016b8339818101604052602081101561003357600080fd5b50516001600160a01b038116610090576040805162461bcd60e51b815260206004820152601660248201527f782063616e6e6f74206265206164647265737328302900000000000000000000604482015290519081900360640190fd5b5060cc8061009f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806326c3b22814602d575b600080fd5b604760048036036020811015604157600080fd5b50356059565b60408051918252519081900360200190f35b6040805182815290516000917f381d78e5942c3f38c806fe97195e3d58555b5c63329a19f958bc104a95c9712f919081900360200190a15080019056fea264697066735822122001bd900349cc18b8054c26ba730cc8ca3355280e24ff51260afc3ead49a7d9d464736f6c634300060a0033'
}
const d0 = await world.deploy(tiny, {args: [], revert: ''})
const d1 = await world.deploy(tiny, {args: [address(0)], revert: 'x cannot be address(0)'})
const d2 = await world.deploy(tiny, {args: [address(1)]})

const c1 = await world.call('tiny', {on: d2.address, args: [1]})
await assert(c1 == 2)

const s1 = await world.send('tiny', {to: d2.address, args: [1], emits: '!TinyLog'}) // no abi!

return {world, tiny, d0, d1, d2, c1, s1}
}
];
const results = {}
for (const test of tests) {
const env = await GanacheEnv()
const filter = t => {
if (t.name == 'fullForkTest')
return false; // change me if forking changes, otherwise skip (requires proxy)
return true; // edit me (e.g. t.name == 'evmLib')
}
results[test.name] = filter(test) ? await test(new World(env), env.bindings()) : 'skipped'
}
return results;
}
Insert cell
//_runTests() // uncomment me during development
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more