Contracts
In order to make it easier to set up and interact with on-chain smart contracts,
there is a contracts
field in the yaml file. The contract field allows you to
instantiate web3 type contract classes inside the simulator. Each contract that
will be interacted with during a simulation should be passed in here.
It is possible to input an already deployed smart contract by providing its abi and deployed address. An alias name for the smart contract should also be provided so you can access the smart contract using that alias during the simulation. It is also possible to leave out the address field. In this scenario, the address can be inserted during the simulation. Such functionality is useful if the smart contract was deployed by a factory, and you want to instantiate the instance during runtime (rather than the start of the simulation).
The abi
parameter is similar to the behavior of a contract,
it can be the ABI JSON straight into this variable. But you can also define a
file link using file://
as leading string and then specify the file. The file
can be specified in a folder either by giving the full path by leading the path
with a /
or the relative path by leading it with .
but also from your home
directory by leading it with ~
.
It is also possible to deploy new contracts at the start of the simulation using
the installable
parameter. Here you need to pass in the bytecode
, and if
required the constructor_input
. This is a very advanced function and for a
demo you should talk to the Almanak team. The one example here is given without
the constructor and deploys a simple incrementor.
To quickly deploy a new instance of a smart contract that already exists on
chain, you can search etherscan for the deployed smart contract, and under the
contract tab, copy the Contract Creation Code
bytecode into the bytecode
field of the yaml. Note, that the above Contract Creation Code
from etherscan
contains the constructor arguments that were used for the already deployed
contract at the end of the bytecode, and those constructor arguments must be
removed for new constructor arguments to be used.
The constructor_input
must be a string of a list, where the order of the list
needs to exactly match the order of the arguments of the smart contract
constructor.
To deploy a proxy contract, you can use the CREATE2
opcode in order to define
a deterministic address where the implementation contract will be deployed to.
Then, first, deploy the implementation smart contract to this deterministic
address. Second, use the same CREATE2
in the proxy contract to set the
implementation address variable in the proxy contract.
A factory contract can be deployed similarly. When deploying instances using the
factory contract, use the CREATE2
opcode in order to have a deterministic
deployed address for the instances. Then provide a getter function which will
retrieve the deployed address based on the arguments used to create the
instance. This getter function can then be used by the agents in your custom
agent code.
Another function that is offered is setting an initial state. This will be done
during simulator initialization and will be setting a certain storageslot
to a
certain value. Again a very advanced function that you can read more about
here.
What is hard here is figuring out the storage slot that something needs to be
written to. This function has not been included in the simulator but the
responsibility of doing this has been passed down to you - the user. This has
been done since it's very hard to determine whether a given slot number
is the correct one that is mapped to the variable name. The recommendation here
is to try to write to the slot locally and use a contract call to retrieve a
value to see if it actually updated. Once you know the storage slot number it
will be the same inside the simulator.
Calling functions
Each contract is unique, that's why we want to give you very low control of the contract so that you can call all the functions on them regardless of the input and outputs of the call.
All contract functions are made available directly on the client
class. This
class can be retrieved through the environment
class using the function
get_client()
. This will return the client which contains all the contracts.
If you've specified the contract with the alias incrementer
you can access it
by directly calling that alias as a variable on the client.
environment.get_client().incrementer
From which you can call the function that
you want to environment.get_client().incrementer.increment()
. From here you
will build the transaction.
If no address was specified with the abi, you can return an instance of the web3
contract by calling the alias and using the deployed address as input. For
example, environment.get_client().alias(deployed_address)
would return a web3
contract instance.
Sending the transaction happens depending on if it's a read or write transaction.
Read functions
To read functions you can use the call()
function, which is a free function to
execute and will return the value of the function.
environment.get_client().incrementer.number().call()
Write functions
For write functions it's a little more complicated, you need to use
transact({'from': from_address})
where the from_address
is the address that
you want to send the transaction from.
environment.get_client().incrementer.increment().transact({'from': ''})
To perform this write function there are a few things you have to keep in mind for it to work:
- You have to pay gas for this transaction. For now you can easily get some ETH
to pay for the gas using the
EVMToken
class which you can read more about here. - The address has to be impersonated. For now we simply impersonate all for simulation purposes but you might run into an error when that isn't the case.
- If you're trying to perform a function that requires access to an ERC20 token
of a user you should make sure the specified address of the contract has been
"Approved" to the correct height of the specified Token. Use the
EVMToken
class to do that, it has anapprove
function. - When sending a native token is required you need to extend the
{'from': ''}
to{'from': '', value: eth_amount}
where theeth_amount
is the amount of native token you want to send in its lowest denominator.
Format
name | type |
---|---|
type | array of { abi: string, alias: string, installable?: { bytecode: string, constructor_input?: string }, address?: string, initState?: array of { storageSlot: string, value: string } } |
required | true |
Example
contracts:
- abi: '[{"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [{"internalType": "uint256", "name": "_value", "type": "uint256"}], "name": "increment", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "number", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "reset", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]'
alias: "incrementer"
installable:
bytecode: "608060405234801561001057600080fd5b50600080819055506101db806100276000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80637cf5dab0146100465780638381f58a14610062578063d826f88f14610080575b600080fd5b610060600480360381019061005b91906100eb565b61008a565b005b61006a6100a1565b6040516100779190610127565b60405180910390f35b6100886100a7565b005b806000546100989190610171565b60008190555050565b60005481565b60008081905550565b600080fd5b6000819050919050565b6100c8816100b5565b81146100d357600080fd5b50565b6000813590506100e5816100bf565b92915050565b600060208284031215610101576101006100b0565b5b600061010f848285016100d6565b91505092915050565b610121816100b5565b82525050565b600060208201905061013c6000830184610118565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017c826100b5565b9150610187836100b5565b925082820190508082111561019f5761019e610142565b5b9291505056fea264697066735822122072e7fdd0a3a63d596b9da980049bd6c53b405daff56912d3871ff9e9f4007e7664736f6c63430008120033"
- abi: "file://./abi/bancor_network.json"
address: "0xeEF417e1D5CC832e619ae18D2F140De2999dD4fB"
alias: "bancor_network"