Creating a Dynamic Contract (Advanced)
A walkthrough on a more advanced Dynamic Contract usage.
Let's create another Dynamic Contract that can be used for depositing and withdrawing ERC20 tokens. This subchapter assumes you went through the previous (Simple) one first, as most of the heavy explanations are there.
This example uses an already existing contract within the project - the ERC20Wrapper
contract. This is due to the fact that ERC20Wrapper
is a very simple contract, while it shows differences between Solidity and AppLayer contracts when calling other contracts. For reference, check the erc20wrapper.h
and erc20wrapper.cpp
files in src/contract/templates
.
Solidity Example
We'll be using the following Solidity code as a reference:
Creating the files
Create the header and source files (erc20wrapper.h
and erc20wrapper.cpp
, as stated above) and add them to CMakeLists.txt
:
Creating the contract header and registering
Inside erc20wrapper.h
, let's implement the header (comments were taken out so it's easier to read):
Here, we recreated the contract's functions but also added a few extra functions (explained in the previous sections). In short, we create:
Two constructors - one for creating the contract from scratch, and another for loading it from the database
The
ConstructorArguments
tuple,registerContract()
andregisterContractFunctions()
functions for proper contract registering (notice that the tuple is required, even though it's empty)The
dump()
function for saving the contract's variablesPrivate SafeVariables (in this case,
SafeUnorderedMap
) to handle the contract's variablesThe contract's functions according to the Solidity signatures
Like in SimpleContract's case, you must include your contract's header in customcontracts.h
to register it, and check it's set to generate its ABI through main-contract-abi.cpp
. In this specific case for ERC20Wrapper
, it's assumed that both steps are already done, but it's good to check again just in case.
Implementing the contract constructors and dumping function
Inside erc20wrapper.cpp
, let's implement both constructors and the dumping function:
One constructor will create a new contract from scratch, as there is no previous existing contract to load, while the other will load the contract from the database when it already exists there. On both cases you are required to initialize, commit and enable registering for all the variables of your contract by hand within the DynamicContract
constructor, as well as calling registerContractFunctions()
, all in the same order as explained in the previous subchapter. The dumping function on the other hand is responsible for saving the current information within the contract back to the database.
Notice that your contract's name ("ERC20Wrapper") is the same as your contract's class name (ERC20Wrapper
) - again, just like with SimpleContract, this match is mandatory, otherwise a segfault will happen. getNewPrefix()
does the same as getDBPrefix()
, but with a user-defined string appended to it, so this would be equivalent to DBPrefix::contracts
+ the contract's address + tokensAndBalances_
.
Implementing the contract functions
This step is pretty straightforward, we just follow the rules explained previously:
Calling functions from another contract
Notice that, in the example above, some functions are calling functions from another contract. This is done by calling callContractViewFunction()
(for view functions) and callContractFunction()
(for non-view/callable functions), both of which require the following arguments:
The other contract's address (in this case,
token
)A reference to the function that will be called - in this case:
getContractBalance()
callsERC20::balanceOf()
withdraw()
andtransferTo()
callERC20::transfer()
deposit()
callsERC20::transferFrom()
The function's arguments, if there's any - in this case:
ERC20::balanceOf()
will receive our contract's own address asgetContractAddress()
ERC20::transfer()
will receive the receiver address asto
orgetCaller()
, and the value to be transferred asvalue
ERC20::transferFrom()
will receive the sender's address asgetCaller()
, the receiver's address asgetContractAddress()
, and the value to be transferred asvalue
Alternatively, for view functions specifically, you can get a pointer to the other contract with getContract()
and call the function directly from it, passing along any required arguments. The getContract()
function itself is automatically protected in the case of casting a wrong typed contract or calling an inexistent contract. So the implementation of getContractBalance()
could also be done like this:
As this contract is consuming the ERC20 balance of another contract, you first need to approve the contract to spend the tokens. This can be done in the same manner as Solidity.
Creating contracts on the fly
As a bonus, it's possible for a Dynamic Contract to create another Dynamic Contract, with the callCreateContract()
function. That way you can have, for example, a function that creates another contract on the fly and retrieve its address (see below), but this is also useful for more advanced things like contract factories, where you can have a templated function that creates contracts on the fly based on the type and parameters passed to it.
The function should be used like this:
This creates a new ERC20
contract with the respective parameters:
The caller address, in this case our own contract's address as
this->getContractAddress()
The gas value, in this case
0
The gas price value, also
0
The caller/transaction value, again,
0
The new contract's constructor parameters, in this case an
ERC20
contract needs the token name ("TestToken"
), its ticker ("TST"
), number of decimals (18
), and the amount of tokens that will be minted at creation (1000000000000000000
, which equals exactly 1 TST with 18 decimals)
Registering the contract's functions
Once we're done with implementing the contract, we must register it. We've already coded the ConstructorArguments
tuple and the registerContract()
function in the header, so all that's left is to override registerContractFunctions()
so we can register the contract's functions.
Compiling and deploying
Finally, go back to the project's root, deploy your local network and your contract using the chain owner's private key, then test your contract's compatibility with your favorite frontend tool. If you wish, you can also write tests for your contract. Check the tests
folder for more information.
Last updated