AdvancedEscrow 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).

Table of contents

Intro

This tutorial dives into Acala EVM+ smart contract development using Hardhat development framework. We will start with the setup, build the smart contract and write deployment 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.
Let’s jump right in!
NOTE: You can refer to the complete code of this tutorial at https://github.com/AcalaNetwork/hardhat-tutorials/tree/master/advanced-escrow​

Setting up

The tutorial project will live in the advanced-escrow/ folder. We can create it using mkdir advanced-escrow. As we will be using Hardhat development framework, we need to initiate the yarn project and add hardhat as a development dependency:
yarn init && yarn add --dev hardhat
NOTE: This example can use the default yarn project settings, which means that all of the prompts can be responded to with pressing enter.
Now that the hardhat dependency is added to the project, we can initiate a simple hardhat project with yarn hardhat:
➜ advanced-escrow yarn hardhat
yarn run v1.22.17
​
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
​
πŸ‘· Welcome to Hardhat v2.8.3 πŸ‘·β€
​
? What do you want to do? …
❯ Create a basic sample project
Create an advanced sample project
Create an advanced sample project that uses TypeScript
Create an empty hardhat.config.js
Quit
When the Hardhat prompt appears, selecting the first option will give us an adequate project skeleton that we can modify.
NOTE: Once again, the default settings from Hardhat are acceptable, so we only need to confirm them using the enter key.
As we will be using the Mandala test network, we need to add it to hardhat.config.js. Networks are added in the module.exports section below the solidity compiler version configuration. We will be adding two networks to the configuration. The local development network, which we will call mandala, and the public test network, which we will call mandalaPubDev:
networks: {
mandala: {
url: 'http://127.0.0.1:8545',
accounts: {
mnemonic: 'fox sight canyon orphan hotel grow hedgehog build bless august weather swarm',
path: "m/44'/60'/0'/0",
},
chainId: 595,
},
mandalaPubDev: {
url: 'https://acala-mandala-adapter.api.onfinality.io/public',
accounts: {
mnemonic: YOUR_MNEMONIC,
path: "m/44'/60'/0'/0",
},
chainId: 595,
timeout: 60000,
},
}
Let’s take a look at the network configurations:
  • url: Used to specify the RPC endpoint of the network
  • accounts: Section to describe how Hardhat should acquire or derive the EVM accounts
  • mnemonic: Mnemonic used to derive the accounts. Add your mnemonic here
  • path: Derivation path to create the accounts from the mnemonic
  • chainId: Specific chain ID of the Mandala chain. The value of 595 is used for both, local development network as well as the public test network
  • timeout: An override value for the built in transaction response timeout. It is needed only on the public test network
With that, our project is ready for development.

Smart contract

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 function.
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.
Hardhat has already created a smart contract within the contracts/ folder when we ran its setup. This smart contract is named Greeter. We will remove it and add our own called AdvancedEscrow:
rm contracts/Greeter.sol && touch contracts/AdvancedEscrow.sol
Now that we have our smart contract file ready, we can place an empty smart contract within it:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
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 escrow
  • beneficiary: The account that is to receive the escrowed funds
  • ingressToken: Address of the token that was used to fund the escrow
  • egressToken: Address of the token that will be used to pay out of the escrow
  • AusdValue: Value of the escrow in AUSD
  • deadline: Block number of the block after which, the escrow will be paid out
  • completed: 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 escrow
  • beneficiary: Address of the account to which the escrow should be released to
  • AusdValue: Value of the escrow represented in the AUSD currency
  • fulfilled: 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.
Your contracts/AdvancedEscrow.sol should look like this:
In order to be able to compile our smart contract with the yarn build command, we need to add the custom build command to package.json. We do this by adding a ”scripts” section below the "devDependencies” section and defining the "build” command within it:
"scripts": {
"build": "hardhat compile"
}
With that, the smart contract can be compiled using:
yarn build

Deploy script

Transaction helper utility

In order to be able to deploy your smart contract to the Acala EVM+ using Hardhat, you need to pass custom transaction parameters to the deploy transactions. We could add them directly to the script, but this becomes cumbersome and repetitive as our project grows. To avoid the repetitiveness, we will create a custom transaction helper utility, which will use calcEthereumTransactionParams from @acala-network/eth-providers dependency.
First we need to add the dependency to the project:
yarn add --dev @acala-network/eth-providers
Now that we have the required dependency added to the project, we can create the utility:
mkdir utils && touch utils/transactionHelpers.js
The calcEthereumTransactionParams is imported at the top of the file and let's define the txParams() below it:
const { calcEthereumTransactionParams } = require("@acala-network/eth-providers");
async function txParams() {
​
}
Within the txParams() function, we set the parameters needed to be passed to the calcEthereumTransactionParams and then assign its return values to the ethParams. At the end of the function we return the gas price and gas limit needed to deploy a smart contract:
const txFeePerGas = '199999946752';
const storageByteDeposit = '100000000000000';
const blockNumber = await ethers.provider.getBlockNumber();
​
const ethParams = calcEthereumTransactionParams({
gasLimit: '31000000',
validUntil: (blockNumber + 100).toString(),
storageLimit: '64001',
txFeePerGas,
storageByteDeposit
});
​
return {
txGasPrice: ethParams.txGasPrice,
txGasLimit: ethParams.txGasLimit
};
In order to be able to use the txParams from our new utility, we have to export it at the bottom of the utility:
module.exports = { txParams };
This concludes the transactionHelper and we can move on to writing the deploy script where we will use it.
Your utils/transactionHelper.js should look like this:

Script

Now that we have our smart contract ready, we can deploy it, so we can use it.
Initiating Hardhat also created a scripts folder and within it a sample script. We will remove it and add our own deploy script instead:
rm scripts/sample-script.js && touch scripts/deploy.js
Let’s add a skeleton main function within the deploy.js and make sure it’s executed when the script is called:
async function main() {
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Now that we have the skeleton deploy script, we can import the txParams from the transactionHelper we added in the subsection above at the top of the file:
const {Β txParams } = require("../utils/transactionHelper");
At the beginning of the main function definition, we will set the transaction parameters, by invoking the txParams:
const ethParams = await txParams();
Now that we have the deploy transaction parameters set, we can deploy the smart contract. We need to get the signer which will be used to deploy the smart contract, then we instantiate the smart contract within the contract factory and deploy it, passing the transaction parameters to the deploy transaction. Once the smart contract is successfully deployed, we will log its address to the console:
const [deployer] = await ethers.getSigners();
const AdvancedEscrow = await ethers.getContractFactory("AdvancedEscrow");
const instance = await AdvancedEscrow.deploy({
gasPrice: ethParams.txGasPrice,
gasLimit: ethParams.txGasLimit,
});
console.log("AdvancedEscrow address:", instance.address);
With that, our deploy script is ready to be run.
Your scripts/deploy.js should look like this:
In order to be able to run the deploy.js script, we need to add a script to the package. Json. To add our custom script to the package.json, we need to place our custom script into the "scripts” section. Let’s add two scripts, one for the local development network and one for the public test network:
"deploy-mandala": "hardhat run scripts/deploy.js --network mandala",
"deploy-mandala:pubDev": "hardhat run scripts/deploy.js --network mandalaPubDev"
With that, we are able to run the deploy script using yarn deploy-mandala or yarn deploy-mandala:pubDev. Using the latter command should result in the following output:
yarn deploy-mandala:pubDev
yarn run v1.22.17
$ hardhat run scripts/deploy.js --network mandalaPubDev
AdvancedEscrow address: 0xcfd3fef59055a7525607694FBcA16B6D92D97Eee
✨ Done in 33.18s.
NOTE: You might encounter some warnings before the deploy script is executed. This is nothing to be alarmed about, just some redundant or outdated dependencies of the dependencies that we imported.

Test

Initiating Hardhat also created a test folder and within it a sample test. We will remove it and add our own test instead:
rm test/sample-test.js && touch test/AdvancedEscrow.js
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 will be using chai's expect and ethers' Contract, ContractFactory and BigNumber. We also require the compiled artifacts of the smart contracts that we will be using within the test. This is why we assign them to the AdvancedEscrowContract, TokenContract and DEXContract constants. In order to be able to test the block based deadlines, we need the ability to force the block generation within tests. While we could use the loop helper utility, we need to have reliable tests that don't fail if loop is not running. For this reason, we import the ApiPromise and WsProvider from @polkadot/api. 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 by using:
yarn add --dev console.mute
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. All of the imports and the empty test look like this:
const { ACA, AUSD, DEX, DOT } = require("@acala-network/contracts/utils/MandalaAddress");
const { expect } = require("chai");
const { Contract, ContractFactory, BigNumber } = require("ethers");
​
const AdvancedEscrowContract = require("../artifacts/contracts/AdvancedEscrow.sol/AdvancedEscrow.json");
const TokenContract = require("@acala-network/contracts/build/contracts/Token.json");
const DEXContract = require("@acala-network/contracts/build/contracts/DEX.json")
const { ApiPromise, WsProvider } = require("@polkadot/api");
​
require('console.mute');
​
const NULL_ADDRESS = "0x0000000000000000000000000000000000000000";
const ENDPOINT_URL = process.env.ENDPOINT_URL || 'ws://127.0.0.1:9944';
const provider = new WsProvider(ENDPOINT_URL);
​
describe("AdvancedEscrow contract", function () {
​
});
To setup for each of the test examples we define the AdvancedEscrow variable that will hold the contract factory for our smart contract. The instance variable 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 Signers and deployerAddress and userAddress variables hold the addresses of these signers. 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. This is why 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 AdvancedEscrow;
let instance;
let ACAinstance;
let AUSDinstance;
let DOTinstance;
let DEXinstance;
let deployer;
let user;
let deployerAddress;
let userAddress;
let api;
​
beforeEach(async function () {
[deployer, user] = await ethers.getSigners();
deployerAddress = await deployer.getAddress();
userAddress = await user.getAddress();
AdvancedEscrow = new ContractFactory(AdvancedEscrowContract.abi, AdvancedEscrowContract.bytecode, deployer);
instance = await AdvancedEscrow.deploy();
await instance.deployed();
ACAinstance = new Contract(ACA, TokenContract.abi, deployer);
AUSDinstance = new Contract(AUSD, TokenContract.abi, deployer);
DOTinstance = new Contract(DOT, TokenContract.abi, deployer);
DEXinstance = new Contract(DEX, DEXContract.abi, deployer);
console.mute();
api = await ApiPromise.create({ provider });
console.resume();
});
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 () {
expect(await instance.numberOfEscrows()).to.equal(0);
});
The Operation section will hold more test examples. We will be checking for the following cases:
  1. 1.
    Initiating an escrow with a beneficiary of 0x0 should revert.
  2. 2.
    Initiating an escrow with a token address of 0x0 should revert.
  3. 3.
    Initiating an escrow with a duration of 0 blocks should revert.
  4. 4.
    Initiating an escrow with the value of escrow being higher than the balance of the smart contract should revert.
  5. 5.
    Upon successfully initiating an escrow, EscrowUpdate should be emitted.
  6. 6.
    Upon successfully initating an escrow, the values defining it should correspont to those passed upon initiation.
  7. 7.
    Initiating an escrow before the current escrow is completed should revert.
  8. 8.
    Trying to set the egress token should revert if the escrow has already been completed.
  9. 9.
    Trying to set the egress token should revert when it is not called by the beneficiary.
  10. 10.
    When egress token is successfully set, the escrow value should be updated.
  11. 11.
    Completing an escrow that was already completed should revert.
  12. 12.
    Completing an escrow while not being the initiator should revert.
  13. 13.
    Escrow should be paid out in AUSD when no egress token is set.
  14. 14.
    Escrow should be paid out in the desired egress token when one is set.
  15. 15.
    When escrow is paid out in egress token, that should not impact the AUSD balance of the beneficiary.
  16. 16.
    When escrow is completed, EscrowUpdate is emitted.
  17. 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 expect(instance.initiateEscrow(NULL_ADDRESS, ACA, 10_000, 10)).to
.be.revertedWith("Escrow: beneficiary_ is 0x0");
});
​
it("should revert when ingress token is 0x0", async function () {
await expect(instance.initiateEscrow(userAddress, NULL_ADDRESS, 10_000, 10)).to
.be.revertedWith("Escrow: ingressToken_ is 0x0");
});
​
it("should revert when period is 0", async function () {
await expect(instance.initiateEscrow(userAddress, ACA, 10_000, 0)).to
.be.revertedWith("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).to.be.below(BigNumber.from("10000"));
​
await expect(instance.initiateEscrow(userAddress, ACA, 10_000, 10)).to
.be.revertedWith("Escrow: contract balance is less than ingress value");
});
​
it("should initate escrow and emit EscrowUpdate when initating escrow", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
const expectedValue = await DEXinstance.getSwapTargetAmount([ACA, AUSD], startingBalance.div(1_000_000));
​
await expect(instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1)).to
.emit(instance, "EscrowUpdate")
.withArgs(deployerAddress, userAddress, expectedValue, false);
});
​
it("should set the values of current escrow when initiating the escrow", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
const expectedValue = await DEXinstance.getSwapTargetAmount([ACA, AUSD], startingBalance.div(1_000_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
​
const blockNumber = await ethers.provider.getBlockNumber();
const currentId = await instance.numberOfEscrows();
const escrow = await instance.escrows(currentId.sub(1));
​
expect(escrow.initiator).to.equal(deployerAddress);
expect(escrow.beneficiary).to.equal(userAddress);
expect(escrow.ingressToken).to.equal(ACA);
expect(escrow.egressToken).to.equal(NULL_ADDRESS);
expect(escrow.AusdValue).to.equal(expectedValue);
expect(escrow.deadline).to.equal(blockNumber + 1);
expect(escrow.completed).to.be.false;
});
​
it("should revert when initiating a new escrow when there is a preexiting active escrow", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
await expect(instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1)).to
.be.revertedWith("Escrow: current escrow not yet completed");
});
​
it("should revert when trying to set the egress token after the escrow has already been completed", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
await api.rpc.engine.createBlock(true /* create empty */, true /* finalize it*/);
await api.rpc.engine.createBlock(true /* create empty */, true /* finalize it*/);
​
await expect(instance.connect(user).setEgressToken(DOT)).to
.be.revertedWith("Escrow: already completed");
});
​
it("should revert when trying to set the egress token while not being the beneficiary", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
​
await expect(instance.connect(deployer).setEgressToken(DOT)).to
.be.revertedWith("Escrow: sender is not beneficiary");
});
​
it("should update the egress token", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
const expectedValue = await DEXinstance.getSwapTargetAmount([ACA, AUSD], startingBalance.div(1_000_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 10);
const blockNumber = await ethers.provider.getBlockNumber();
​
await instance.connect(user).setEgressToken(DOT);
​
const currentId = await instance.numberOfEscrows();
const escrow = await instance.escrows(currentId.sub(1));
​
expect(escrow.initiator).to.equal(deployerAddress);
expect(escrow.beneficiary).to.equal(userAddress);
expect(escrow.ingressToken).to.equal(ACA);
expect(escrow.egressToken).to.equal(DOT);
expect(escrow.AusdValue).to.equal(expectedValue);
expect(escrow.deadline).to.equal(blockNumber + 10);
expect(escrow.completed).to.be.false;
});
​
it("should revert when trying to complete an already completed escrow", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
await api.rpc.engine.createBlock(true /* create empty */, true /* finalize it*/);
await api.rpc.engine.createBlock(true /* create empty */, true /* finalize it*/);
​
await expect(instance.connect(deployer).completeEscrow()).to
.be.revertedWith("Escrow: escrow already completed");
});
​
it("should revert when trying to complete an escrow when not being the initiator", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
​
await expect(instance.connect(user).completeEscrow()).to
.be.revertedWith("Escrow: caller is not initiator or this contract");
});
​
it("should pay out the escrow in AUSD if no egress token is set", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
const initalBalance = await AUSDinstance.balanceOf(userAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
​
await instance.connect(deployer).completeEscrow();
const finalBalance = await AUSDinstance.balanceOf(userAddress);
​
expect(finalBalance).to.be.above(initalBalance);
});
​
it("should pay out the escrow in set token when egress token is set", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
const initalBalance = await DOTinstance.balanceOf(userAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 10);
await instance.connect(user).setEgressToken(DOT);
​
await instance.connect(deployer).completeEscrow();
const finalBalance = await DOTinstance.balanceOf(userAddress);
​
expect(finalBalance).to.be.above(initalBalance);
});
​
it("should not pay out the escrow in set AUSD when egress token is set", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
const initalBalance = await AUSDinstance.balanceOf(userAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 10);
await instance.connect(user).setEgressToken(DOT);
​
await instance.connect(deployer).completeEscrow();
const finalBalance = await AUSDinstance.balanceOf(userAddress);
​
expect(finalBalance).to.equal(initalBalance);
});
​
it("should emit EscrowUpdate when escrow is completed", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
const expectedValue = await DEXinstance.getSwapTargetAmount([ACA, AUSD], startingBalance.div(1_000_000));
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 10);
​
await expect(instance.connect(deployer).completeEscrow()).to
.emit(instance, "EscrowUpdate")
.withArgs(deployerAddress, userAddress, expectedValue, true);
});
​
it("should automatically complete the escrow when given number of blocks has passed", async function () {
const startingBalance = await ACAinstance.balanceOf(deployerAddress);
​
await ACAinstance.connect(deployer).transfer(instance.address, startingBalance.div(100_000));
​
await instance.connect(deployer).initiateEscrow(userAddress, ACA, startingBalance.div(1_000_000), 1);
const currentEscrow = await instance.numberOfEscrows();
const initalState = await instance.escrows(currentEscrow.sub(1));
await api.rpc.engine.createBlock(true /* create empty */, true /* finalize it*/);
await api.rpc.engine.createBlock(true /* create empty */, true /* finalize it*/);
const finalState = await instance.escrows(currentEscrow.sub(1));
​
expect(initalState.completed).to.be.false;
expect(finalState.completed).to.be.true;
});
This concludes our test.
Your test/AdvancedEscrow.js should look like this:
As our test is ready to be run, we have to add the scripts to be able to run the test. We will be adding two scripts. One to run the tests on the local development network and on the public test network:
"test-mandala": "hardhat test test/AdvancedEscrow.js --network mandala",