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.
Contracts can be made available through both environments
and protocols
. For
both they are passed in as an argument in the __init__
function.
Environments have access through the EnvironmentHelper
which exposes a
function get_contract
which you can use to retrieve the contract. It also
accepts an address to use with dynamic contracts. This function returns a
ContractInterface
.
Protocols get passed a list of ContractInterface
which you can use to call
functions on the contract.
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.
incrementer_contract.functions.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.
incrementer_contract.functions.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.
Events
You can also access the contract events by calling.
incrementer_contract.events()
Example Environment
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"
Example Protocol
supported_chains:
- engine: 'evm'
chainId: '1'
dependencies:
contracts:
- abi: "file://abi/aave_pool.json"
address: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"
alias: "aave_pool"