Advanced Escrow tutorial
Tutorial on how to build an Escrow smart contract using Acala EVM+'s built in predeployed tokens, DEX and Schedule (an on-chain automation tool).
This tutorial dives into Acala EVM+ smart contract development using Truffle development framework. We will start with the setup, build the smart contract and write deployment, test and user journey scripts. The smart contract will allow users to initiate escrows in one currency, and for beneficiaries to specify if they desire to be paid in another currency. Another feature we will familiarise ourselves with is the on-chain automation using a predeployed smart contract called
Schedule
. Using it will allow us to set the automatic completion of escrow after a certain number of blocks are included in the blockchain.NOTE: You can refer to the complete code of this tutorial at https://github.com/AcalaNetwork/truffle-tutorials/tree/master/advanced-escrow
Let's jump right in!
Assuming you have Truffle and yarn installed, we can jump right into creating a new Truffle project.
- 1.Open a terminal window in a directory where you want your
AdvancedEscrow
example to reside, create a directory for it and then initialize a yarn project within it, as well as add Truffle as a dependency, with the following commands:
mkdir AdvancedEscrow
cd AdvancedEscrow
yarn init --yes
yarn add truffle
truffle init
In addition to initiating a Truffle project, Truffle has already created
contracts
, migrations
and test
directories that we require for this tutorial.As we will be using Truffle to compile, test and deploy the smart contract, we need to configure it. Uncomment the following line in the
truffle-config.js
:For me this is line 21, so even if there is any discrepancy between Truffle versions, this should narrow down the search.
const HDWalletProvider = require('@truffle/hdwallet-provider');
As you may have noticed, we are importing
@truffle/hdwallet-provider
, so we need to add it to the project. Let's add it as a development dependency:yarn add --dev @truffle/hdwallet-provider
We will be enabling a Mandala local development network by adding it to the configuration. Let's add the its configuration. As the public test network has the same configuration as the local development network, let's add a helper function to the config. Make sure to add this helper method above the
module.exports
(you can place it below as well, just not within). We will call it mandalaConfig
and expect one argument to be passed to it. The argument passed to it is called the endpointUrl
and it specifies the RPC endpoint, to which the Truffle connects to. Copy the method into your truffle-config.js
:const mandalaConfig = (endpointUrl) => ({
provider: () =>
new HDWalletProvider(mnemonicPhrase, endpointUrl),
network_id: 595,
gasPrice: 0x2f82f103ea, // storage_limit = 64001, validUntil = 360001, gasLimit = 10000000
gas: 0x329b140,
timeoutBlocks: 25,
confirmations: 0
});
Let's break down this configuration:
provider
uses theHDWalletProvided
that we imported. We pass the mnemonic prase, from which the accounts are derived, as well as the URL for the RPC endpoint of the desired network. You might have noticed that we haven't specifiedmnemonicPhrase
anywhere just yet. Let's do it. Above themandalaConfig
add the following mnemonic:
const mnemonicPhrase = 'fox sight canyon orphan hotel grow hedgehog build bless august weather swarm';
NOTE: This mnemonic phrase is used in all of the examples and represents the default development accounts of Acala EVM+. These accounts are not safe to use and you should use your own, following the secret management guidelines of
HDWalletProvider
.Now that we analyzed the
provider
, let's move on to the other arguments:network_id
is the default network ID of the development Mandala network.gasPrice
is the default gas price for the local development Mandala network. Commented out section represents additional parameters of Acala EVM+.gas
is the current gas limit for transactions.timeoutBlocks
andconfirmations
are set to our discretion and we opted for the values above in this tutorial.
The gas limit and gas price of the example might get out of sync as new blocks are mined on Mandala test network. If you encounter OutOfStorage error, or any other for that matter, while trying to deploy your smart contract using the Remix IDE, we suggest verifying these values following these instructions.
To be able to use the local development network and public test network within the project, we have to add them to the
networks
section of the config using the mandalaConfig
helper. We do this by pasting the following two lines of code into it: mandala: mandalaConfig("http://127.0.0.1:8545"),
mandalaPublicDev: mandalaConfig("https://eth-rpc-mandala.aca-staging.network"),
Now that Mandala local development network is added to our project, let's take care of the remaining configuration. Mocha timeout should be active, to make sure that we don't get stuck in a loop if something goes wrong during tests. For this line 91 (this is after the modifications) in
truffle-config.js
should be uncommented: timeout: 100000
Lastly, let's set the compiler version to
0.8.9
as this is the Solidity version we will be using in our example smart contract. To do this, line 97 needs to be uncommented and modified to: version: "0.8.9", // Fetch exact version from solc-bin (default: truffle's version)
The
AdvancedEscrow
smart contract, which we will add in the following section, will still leave some areas that could be improved. Advanced
is referring to the use of the predeployed smart contracts in the Acala EVM+ rather than its operation.When two parties enter into an escrow agreement, using the
AdvancedEscrow
smart contract, the party paying for the service first transfers the tokens from one of the predeployed ERC20 smart contracts into the escrow smart contract. The party then initiates the escrow within the smart contract. Initiation of escrow requires both the contract address of the token being escrowed, and the wallet address of the beneficiary of escrow.Upon initiation of the escrow, the smart contract exchanges the tokens coming into escrow for AUSD. Then it sets the deadline after which AUSD is released to the beneficiary. The beneficiary also has the ability to specify which tokens they want to receive from escrow and the smart contract exchanges the AUSD it is holding in escrow for the desired tokens upon completion of escrow.
We also allow for the escrow to be completed before the deadline, with the ability for the initiating party to release the funds to the beneficiary manually.
In order to add our smart contract, we will use Truffle built-in utility
create
:truffle create contract AdvancedEscrow
This command created a
HelloWorld.sol
file with a skeleton smart contract within contracts
directory. First line of this smart contract, after the license definition, should specify the exact version of Solidity we will be using, which is 0.8.9
:// SPDX-License-Identifier: MIT
pragma solidity =0.8.9;
contract AdvancedEscrow {
}
We will be using precompiled smart contracts available in
@acala-network/contracts
and @openzeppelin/contracts
dependencies. To be able to do this, we need to add the dependencies to the project:yarn add --dev @acala-network/[email protected] @openzeppelin/[email protected]
As we will be using predeployed IDEX and IScheduler as well as the precompiled Token contracts, we need to import them after the
pragma
statement:import "@acala-network/contracts/dex/IDEX.sol";
import "@acala-network/contracts/token/Token.sol";
import "@acala-network/contracts/schedule/ISchedule.sol";
As each of the predeployed smart contracts has a predetermined address, we can use one of the
Address
utilities of @acala-network/contracts
dependency to set them in our smart contract. There are the AcalaAddress
, the KaruraAddress
and the MandalaAddress
utilities. We can use the MandalaAddress
in this example:import "@acala-network/contracts/utils/MandalaAddress.sol";
Now that we have sorted out all of the imports, we need to make sure that our
AdvancedEscrow
smart contract inherits the ADDRESS
smart contract utility in order to be able to access the addresses of the predeployed contracts stored within it. We have to add the inheritance statement to the contract definition line:contract AdvancedEscrow is ADDRESS {
We can finally start working on the actual smart contract. We will be interacting with the predeployed DEX and Schedule smart contracts, so we can define them at the beginning of the smart contract:
IDEX public dex = IDEX(ADDRESS.DEX);
ISchedule public schedule = ISchedule(ADDRESS.SCHEDULE);
Our smart contract will support one active escrow at the time, but will allow reuse. Let’s add a counter to be able to check the previous escrows, as well as the Escrow structure:
uint256 public numberOfEscrows;
mapping(uint256 => Escrow) public escrows;
struct Escrow {
address initiator;
address beneficiary;
address ingressToken;
address egressToken;
uint256 AusdValue;
uint256 deadline;
bool completed;
}
As you can see, we added a counter for
numberOfEscrows
, a mapping to list said escrows
and a struct to keep track of the information included inside an escrow. The Escrow
structure holds the following information:initiator
: The account that initiated and funded the escrowbeneficiary
; The account that is to receive the escrowed fundsingressToken
: Address of the token that was used to fund the escrowegressToken
: Address of the token that will be used to pay out of the escrowAusdValue
: Value of the escrow in AUSDdeadline
: Block number of the block after which, the escrow will be paid outcompleted
: As an escrow can only be active or fulfilled, this can be represented as by a boolean value.
The constructor in itself will only be used to set the value of
numberOfEscrows
to 0. While Solidity is a null-state language, it’s still better to be explicit where we can: constructor() {
numberOfEscrows = 0;
}
Now we can add the event that will notify listeners of the change in the smart contract called
EscrowUpdate
: event EscrowUpdate(
address indexed initiator,
address indexed beneficiary,
uint256 AusdValue,
bool fulfilled
);
The event contains information about the current state of the latest escrow:
initiator
: Address of the account that initiated the escrowbeneficiary
: Address of the account to which the escrow should be released toAusdValue
: Value of the escrow represented in the AUSD currencyfulfilled
: As an escrow can only be active or fulfilled, this can be represented as by a boolean value.
Let’s start writing the logic of the escrow. As we said, there should only be one escrow active at any given time and the initiator should transfer the tokens to the smart contract before initiating the escrow. When initiating escrow, the initiator should pass the address of the token they allocated to the smart contract as the function call parameter in order for the smart contract to be able to swap that token for AUSD. All of the escrows are held in AUSD, but they can be paid out in an alternative currency. None of the addresses passed to the function should be
0x0
and the period in which the escrow should automatically be completed, expressed in the number of blocks, should not be 0 as well.Once all of the checks are passed and the ingress tokens are swapped for AUSD, the completion of escrow should be scheduled with the predeployed
Schedule
. Afterwards, the escrow information should be saved to the storage and EscrowUpdate
should be emitted.All of this happens within
initiateEscrow
function: function initiateEscrow(
address beneficiary_,
address ingressToken_,
uint256 ingressValue,
uint256 period
)
public returns (bool)
{
// Check to make sure the latest escrow is completed
// Additional check is needed to ensure that the first escrow can be initiated and that the
// guard statement doesn't underflow
require(
numberOfEscrows == 0 || escrows[numberOfEscrows - 1].completed,
"Escrow: current escrow not yet completed"
);
require(beneficiary_ != address(0), "Escrow: beneficiary_ is 0x0");
require(ingressToken_ != address(0), "Escrow: ingressToken_ is 0x0");
require(period != 0, "Escrow: period is 0");
uint256 contractBalance = Token(ingressToken_).balanceOf(address(this));
require(
contractBalance >= ingressValue,
"Escrow: contract balance is less than ingress value"
);
Token AUSDtoken = Token(ADDRESS.AUSD);
uint256 initalAusdBalance = AUSDtoken.balanceOf(address(this));
address[] memory path = new address[](2);
path[0] = ingressToken_;
path[1] = ADDRESS.AUSD;
require(dex.swapWithExactSupply(path, ingressValue, 1), "Escrow: Swap failed");
uint256 finalAusdBalance = AUSDtoken.balanceOf(address(this));
schedule.scheduleCall(
address(this),
0,
1000000,
5000,
period,
abi.encodeWithSignature("completeEscrow()")
);
Escrow storage currentEscrow = escrows[numberOfEscrows];
currentEscrow.initiator = msg.sender;
currentEscrow.beneficiary = beneficiary_;
currentEscrow.ingressToken = ingressToken_;
currentEscrow.AusdValue = finalAusdBalance - initalAusdBalance;
currentEscrow.deadline = block.number + period;
numberOfEscrows += 1;
emit EscrowUpdate(msg.sender, beneficiary_, currentEscrow.AusdValue, false);
return true;
}
As you might have noticed, we didn’t set the
egressToken
value of the escrow. This is up to the beneficiary. Default payout is AUSD; but the beneficiary should be able to set a different token if they wish. As this is completely their prerogative, they are the only party that can change this value. To be able to do so, we need to add an additional setEgressToken
function. Only the latest escrow’s egress token value can be modified and only if the latest escrow is still active: function setEgressToken(address egressToken_) public returns (bool) {
require(!escrows[numberOfEscrows - 1].completed, "Escrow: already completed");
require(
escrows[numberOfEscrows - 1].beneficiary == msg.sender,
"Escrow: sender is not beneficiary"
);
escrows[numberOfEscrows - 1].egressToken = egressToken_;
return true;
}
Another thing that you might have noticed is that we scheduled a call of
completeEscrow
in the scheduleCall
call to the Schedule
predeployed smart contract. We need to add this function as well. The function should only be able to be run if the current escrow is still active and only by the AdvancedEscrow
smart contract or by the initiator of the escrow. The smart contract is able to call the completeEscrow
function, because it passed a pre-signed transaction for this call to the Schedule
smart contract. The function should swap the AUSD held in escrow for the desired egress token, if one is specified. Otherwise, the AUSD is released to the beneficiary. Once the funds are allocated to the beneficiary, the escrow should be marked as completed and EscrowUpdate
event, notifying the listeners of the completion, should be emitted: function completeEscrow() public returns (bool) {
Escrow storage currentEscrow = escrows[numberOfEscrows - 1];
require(!currentEscrow.completed, "Escrow: escrow already completed");
require(
msg.sender == currentEscrow.initiator || msg.sender == address(this),
"Escrow: caller is not initiator or this contract"
);
if(currentEscrow.egressToken != address(0)){
Token token = Token(currentEscrow.egressToken);
uint256 initialBalance = token.balanceOf(address(this));
address[] memory path = new address[](2);
path[0] = ADDRESS.AUSD;
path[1] = currentEscrow.egressToken;
require(
dex.swapWithExactSupply(path, currentEscrow.AusdValue, 1),
"Escrow: Swap failed"
);
uint256 finalBalance = token.balanceOf(address(this));
token.transfer(currentEscrow.beneficiary, finalBalance - initialBalance);
} else {
Token AusdToken = Token(ADDRESS.AUSD);
AusdToken.transfer(currentEscrow.beneficiary, currentEscrow.AusdValue);
}
currentEscrow.completed = true;
emit EscrowUpdate(
currentEscrow.initiator,
currentEscrow.beneficiary,
currentEscrow.AusdValue,
true
);
return true;
}
This wraps up our
AdvancedEscrow
smart contract.// SPDX-License-Identifier: MIT
pragma solidity =0.8.9;
import "@acala-network/contracts/dex/IDEX.sol";
import "@acala-network/contracts/token/Token.sol";
import "@acala-network/contracts/schedule/ISchedule.sol";
import "@acala-network/contracts/utils/MandalaAddress.sol";
contract AdvancedEscrow is ADDRESS {
IDEX dex = IDEX(ADDRESS.DEX);
ISchedule schedule = ISchedule(ADDRESS.SCHEDULE);
uint256 public numberOfEscrows;
mapping(uint256 => Escrow) public escrows;
struct Escrow {
address initiator;
address beneficiary;
address ingressToken;
address egressToken;
uint256 AusdValue;
uint256 deadline;
bool completed;
}
constructor() {
numberOfEscrows = 0;
}
event EscrowUpdate(
address indexed initiator,
address indexed beneficiary,
uint256 AusdValue,
bool fulfilled
);
function initiateEscrow(
address beneficiary_,
address ingressToken_,
uint256 ingressValue,
uint256 period
)
public returns (bool)
{
// Check to make sure the latest escrow is completed
// Additional check is needed to ensure that the first escrow can be initiated and that the
// guard statement doesn't underflow
require(
numberOfEscrows == 0 || escrows[numberOfEscrows - 1].completed,
"Escrow: current escrow not yet completed"
);
require(beneficiary_ != address(0), "Escrow: beneficiary_ is 0x0");
require(ingressToken_ != address(0), "Escrow: ingressToken_ is 0x0");
require(period != 0, "Escrow: period is 0");
uint256 contractBalance = Token(ingressToken_).balanceOf(address(this));
require(
contractBalance >= ingressValue,
"Escrow: contract balance is less than ingress value"
);
Token AUSDtoken = Token(ADDRESS.AUSD);
uint256 initalAusdBalance = AUSDtoken.balanceOf(address(this));
address[] memory path = new address[](2);
path[0] = ingressToken_;
path[1] = ADDRESS.AUSD;
require(dex.swapWithExactSupply(path, ingressValue, 1), "Escrow: Swap failed");
uint256 finalAusdBalance = AUSDtoken.balanceOf(address(this));
schedule.scheduleCall(
address(this),
0,
1000000,
5000,
period,
abi.encodeWithSignature("completeEscrow()")
);
Escrow storage currentEscrow = escrows[numberOfEscrows];
currentEscrow.initiator = msg.sender;
currentEscrow.beneficiary = beneficiary_;
currentEscrow.ingressToken = ingressToken_;
currentEscrow.AusdValue = finalAusdBalance - initalAusdBalance;
currentEscrow.deadline = block.number + period;
numberOfEscrows += 1;
emit EscrowUpdate(msg.sender, beneficiary_, currentEscrow.AusdValue, false);
return true;
}
function setEgressToken(address egressToken_) public returns (bool) {
require(!escrows[numberOfEscrows - 1].completed, "Escrow: already completed");
require(
escrows[numberOfEscrows - 1].beneficiary == msg.sender,
"Escrow: sender is not beneficiary"
);
escrows[numberOfEscrows - 1].egressToken = egressToken_;
return true;
}
function completeEscrow() public returns (bool) {
Escrow storage currentEscrow = escrows[numberOfEscrows - 1];
require(!currentEscrow.completed, "Escrow: escrow already completed");
require(
msg.sender == currentEscrow.initiator || msg.sender == address(this),
"Escrow: caller is not initiator or this contract"
);
if(currentEscrow.egressToken != address(0)){
Token token = Token(currentEscrow.egressToken);
uint256 initialBalance = token.balanceOf(address(this));
address[] memory path = new address[](2);
path[0] = ADDRESS.AUSD;
path[1] = currentEscrow.egressToken;
require(
dex.swapWithExactSupply(path, currentEscrow.AusdValue, 1),
"Escrow: Swap failed"
);
uint256 finalBalance = token.balanceOf(address(this));
token.transfer(currentEscrow.beneficiary, finalBalance - initialBalance);
} else {
Token AusdToken = Token(ADDRESS.AUSD);
AusdToken.transfer(currentEscrow.beneficiary, currentEscrow.AusdValue);
}
currentEscrow.completed = true;
emit EscrowUpdate(
currentEscrow.initiator,
currentEscrow.beneficiary,
currentEscrow.AusdValue,
true
);
return true;
}
}
Now that we have the smart contract ready, we have to compile it. For this, we will add the
build
script to the package.json
. To do this, we have to add scripts
section to it. We will be using Truffle's compile functionality, so the scripts
section should look like this: "scripts": {
"build": "truffle compile"
}
When you run the
build
command using yarn build
, the build
directory is created and it contains the compiled smart contract.Now that we have our smart contract ready, we can deploy it, so we can use it. We can again use the Truffle built-in utility
create
to create a migration file:truffle create migration AdvancedEscrow
The utility created a barebones migration file in the
migrations
folder. First thing we need to do is import our smart contract into it. We do this with the following line of code at the top of the file:const AdvancedEscrow = artifacts.require('AdvancedEscrow');
To make sure that our migration will successfully deploy our smart contract, we have to make sure that our
deployer
is ready. To do that, we need to modfy the deployment function to be asynchronous. Replace the 3rd line of the migration with:module.exports = async function (deployer) {
Now that we have the smart contract imported within the migration, we can deploy the smart contract. We do this by invoking
deployer
, which is defined in the definition of the function. Additionally we will output the address of the deployed smart contract: console.log('Deploy AdvancedEscrow');
await deployer.deploy(AdvancedEscrow);
console.log(`Advanced escrow deployed at: ${AdvancedEscrow.address}`);
This completes our migration and allows us to deploy the example smart contract as well as run tests for it.
To run the migration and deploy the smart contract, we have to add the scripts to deploy the smart contract to the local development network as well as public Mandala test network. To do this, we have to add them to the
scripts
section of the package.json
: "deploy-mandala": "truffle migrate --network mandala",
"deploy-mandala:pubDev": "truffle migrate --network mandalaPublicDev"
Deploying the
AdvancedEscrow
smart contract using yarn deploy-mandala
command should return the following output:yarn deploy-mandala
yarn run v1.22.18
$ truffle migrate --network mandala
Compiling your contracts...
===========================
> Compiling ./../DEX/contracts/PrecompiledDEX.sol
> Artifacts written to /Users/jan/Acala/truffle-tutorials/AdvancedEscrow/build/contracts
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Starting migrations...
======================
> Network name: 'mandala'
> Network id: 595
> Block gas limit: 15000000 (0xe4e1c0)
1_initial_migration.js
======================
Deploying 'Migrations'
----------------------
> transaction hash: 0x315d14d6fd5e6640b981537044b653774002bcfbbe9ff8cc8f6c54502328c8bc
> Blocks: 0 Seconds: 0
> contract address: 0x46CD18A2CE038D21b78dC3EF470CCf9Dc586AEa4
> block number: 3871
> block timestamp: 1652125822
> account: 0x75E480dB528101a381Ce68544611C169Ad7EB342
> balance: 9967868.909147408268
> gas used: 250142 (0x3d11e)
> gas price: 10770.794810139 gwei
> value sent: 0 ETH
> total cost: 2.694228155397789738 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 2.694228155397789738 ETH
1651609607_advanced_escrow.js
=============================
Deploy AdvancedEscrow
Deploying 'AdvancedEscrow'
--------------------------
> transaction hash: 0x21a8bf1d1ec696d9ca0e2cf2a02919ea73483533b630392db596cfa59d18e7d0
> Blocks: 0 Seconds: 0
> contract address: 0xF49B534C00Fbeb4E7B055BCbAcDAC161BC4090F5
> block number: 3873
> block timestamp: 1652125834
> account: 0x75E480dB528101a381Ce68544611C169Ad7EB342
> balance: 9967865.664529159889
> gas used: 2262155 (0x22848b)
> gas price: 2010.260490745 gwei
> value sent: 0 ETH
> total cost: 4.547520820441255475 ETH
Advanced escrow deployed at: 0xF49B534C00Fbeb4E7B055BCbAcDAC161BC4090F5
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 4.547520820441255475 ETH
Summary
=======
> Total deployments: 2
> Final cost: 7.241748975839045213 ETH
✨ Done in 5.64s.
To add a test for the smart contract, we can again use the Truffle built-in
create
utility:truffle create test AdvancedEscrow
At the beginning of the file, we will be importing all of the constants and methods that we will require to successfully run the tests. The predeploy smart contract addresses are imported from
@acala-network/contracts
ADDRESS
utility. We require the compiled artifacts of the smart contracts that we will be using within the test. This is why we assign them to the AdvancedEscrow
, PrecompiledToken
and PrecompiledDEX
constants. In order to be able to test the block based deadlines, we need the ability to force the block generation within tests. For this reason, we import the ApiPromise
and WsProvider
from @polkadot/api
, which we need to add to the project. As initiating the ApiPromise
generates a lot of output, our test output would get very messy if we didn't silence it. To do this we use the console.mute
dependency, that we have to add to the project, along with @polkadot/api
, by using:yarn add --dev console.mute @polkadot/api
In order to be able to validate the expected reverts and event emissions, we will use
truffleAssert
method from truffle-assertions
dependency, which we import using:yarn add --dev truffle-assertions
To be able to easily validate things dependent on
0x0
address, we assign it to the NULL_ADDRESS
constant. Lastly we configure the ENDPOINT_URL
constant to be used by the provider
. And instantiate the WsProvider
to the provider
constant. The test file with import statements and an empty test should look like this:const AdvancedEscrow = artifacts.require('AdvancedEscrow');
const PrecompiledDEX = artifacts.require('IDEX');
const PrecompiledToken = artifacts.require('Token');
const { ApiPromise, WsProvider } = require('@polkadot/api');
const truffleAssert = require('truffle-assertions');
require('console.mute');
const { ACA, AUSD, DOT, DEX } = require('@acala-network/contracts/utils/MandalaAddress');
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const ENDPOINT_URL = process.env.ENDPOINT_URL || 'ws://127.0.0.1:9944';
const provider = new WsProvider(ENDPOINT_URL);
/*
* uncomment accounts to access the test accounts made available by the
* Ethereum client
* See docs: https://www.trufflesuite.com/docs/truffle/testing/writing-tests-in-javascript
*/
contract('AdvancedEscrow', function (accounts) {
});
To setup for each of the test examples we define the
instance
variable which will hold the instance of the smart contract that we will be testing against and the ACAinstance
, AUSDinstance
, DOTinstance
and DEXinstance
hold the instances of the predeployed smart contracts. The deployer
and user
hold the accounts we will be using within the tests. Finally the api
variable holds the ApiPromise
, which we will use to force the generation of blocks. As creation of ApiPromise
generates a lot of console output, especially when being run before each of the test examples, we have to mute the console output before we create it and resume it after, to keep the expected behaviour of the console. All of the values are assigned in the beforeEach
action: let instance;
let ACAinstance;
let AUSDinstance;
let DOTinstance;
let DEXinstance;
let deployer;
let user;
let api;
beforeEach('setup development environment', async function () {
[deployer, user] = accounts;
instance = await AdvancedEscrow.new();
ACAinstance = await PrecompiledToken.at(ACA);
AUSDinstance = await PrecompiledToken.at(AUSD);
DOTinstance = await PrecompiledToken.at(DOT);
DEXinstance = await PrecompiledDEX.at(DEX);
console.mute();
api = await ApiPromise.create({ provider });
console.resume();
});
NOTE: You can see how we used the
ACA
, AUSD
, DOT
and DEX
from the ADDRESS
utility in order to set the addresses of our predeployed smart contract.Our test cases will be split into two groups. One will be called
Deployment
and it will verify that the deployed smart contract has expected values set before it is being used. The second one will be called Operation
and it will validate the expected behaviour of our smart contract. The empty sections should look like this: describe('Deployment', function () {
});
describe('Operation', function () {
});
We will only have one example within the
Deployment
section and it will verify that the number of escrows in a newly deployed smart contract is set to 0
: it('should set the initial number of escrows to 0', async function () {
const numberOfEscrows = await instance.numberOfEscrows();
expect(numberOfEscrows.isZero()).to.be.true;
});
The
Operation
section will hold more test examples. We will be checking for the following cases:- 1.Initiating an escrow with a beneficiary of
0x0
should revert. - 2.Initiating an escrow with a token address of
0x0
should revert. - 3.Initiating an escrow with a duration of
0
blocks should revert. - 4.Initiating an escrow with the value of escrow being higher than the balance of the smart contract should revert.
- 5.Upon successfully initiating an escrow,
EscrowUpdate
should be emitted. - 6.Upon successfully initiating an escrow, the values defining it should correspond to those passed upon initiation.
- 7.Initiating an escrow before the current escrow is completed should revert.
- 8.Trying to set the egress token should revert if the escrow has already been completed.
- 9.Trying to set the egress token should revert when it is not called by the beneficiary.
- 10.When egress token is successfully set, the escrow value should be updated.
- 11.Completing an escrow that was already completed should revert.
- 12.Completing an escrow while not being the initiator should revert.
- 13.Escrow should be paid out in AUSD when no egress token is set.
- 14.Escrow should be paid out in the desired egress token when one is set.
- 15.When escrow is paid out in egress token, that should not impact the AUSD balance of the beneficiary.
- 16.When escrow is completed,
EscrowUpdate
is emitted. - 17.Escrow should be completed automatically when the desired number of blocks has passed.
These are the examples outlined above:
it('should revert when beneficiary is 0x0', async function () {
await truffleAssert.reverts(
instance.initiateEscrow(NULL_ADDRESS, ACA, 10_000, 10, { from: deployer }),
'Escrow: beneficiary_ is 0x0'
);
});
it('should revert when ingress token is 0x0', async function () {
await truffleAssert.reverts(
instance.initiateEscrow(user, NULL_ADDRESS, 10_000, 10, { from: deployer }),
'Escrow: ingressToken_ is 0x0'
);
});
it('should revert when period is 0', async function () {
await truffleAssert.reverts(
instance.initiateEscrow(user, ACA, 10_000, 0, { from: deployer }),
'Escrow: period is 0'
);
});
it('should revert when balance of the contract is lower than ingressValue', async function () {
const balance = await ACAinstance.balanceOf(instance.address);
expect(balance.lt(web3.utils.toBN('10000'))).to.be.true;
await truffleAssert.reverts(
instance.initiateEscrow(user, ACA, 10_000, 10, { from: deployer }),
'Escrow: contract balance is less than ingress value'
);
});
it('should initiate escrow and emit EscrowUpdate when initializing escrow', async function () {
const startingBalance = await ACAinstance.balanceOf(deployer);
await ACAinstance.transfer(instance.address, Math.floor(startingBalance/100000), { from: deployer });
const expectedValue = await DEXinstance.getSwapTargetAmount([ACA, AUSD], Math.floor(startingBalance/1000000));
truffleAssert.eventEmitted(
await instance.initiateEscrow(user, ACA, Math.floor(startingBalance/1000000), 1, { from: deployer }),
'EscrowUpdate',
{
initiator: deployer,
beneficiary: user,
AusdValue: expectedValue,
fulfilled: false
}
);
});
it('should set the values of current escrow when initiating the escrow', async function () {
const startingBalance = await ACAinstance.balanceOf(deployer);
await ACAinstance.transfer(instance.address, Math.floor(startingBalance/100000), { from: deployer });
const expectedValue = await DEXinstance.getSwapTargetAmount([ACA, AUSD], Math.floor(startingBalance/1000000));
await instance.initiateEscrow(user, ACA, Math.floor(startingBalance/1000000), 1, { from: deployer });
const blockNumber = await web3.eth.getBlock('latest');
const currentId = await instance.numberOfEscrows();
const escrow = await instance.escrows(currentId - 1);
expect(escrow.initiator).to.equal(deployer);
expect(escrow.beneficiary).to.equal(user);
expect(escrow.ingressToken).to.equal(ACA);
expect(escrow.egressToken).to.equal(NULL_ADDRESS);
expect(escrow.AusdValue.eq(expectedValue)).to.be.true;
expect(escrow.deadline == (blockNumber.number + 1)).to.be.true;
expect(escrow.completed).to.be.false;
});
it('should revert when initiating a new escrow when there is a preexisting active escrow', async function () {
const startingBalance = await ACAinstance.balanceOf(deployer);
await ACAinstance.transfer(instance.address, Math.floor(startingBalance/100000), { from: deployer });