Skip to main content

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 an approve function.
  • When sending a native token is required you need to extend the {'from': ''} to {'from': '', value: eth_amount} where the eth_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"