In this first part of this series, we are going to learn how to create an smart contract which allows two users to swap their houses. You can read the full contract code here
How the contract works
The following sections describes how the contract works without diving into the peculiarities of each method but showing an overview of the contract flow.
Init the contract
The contract owner (the one who deploys it) call initialize function. That function sets contract status to INITIALIZED and stores house data so that other users can query it and make swap offers.
Only the contract owner can invoke this function.
Add a new offer
A user can call addOffer function to send a swap offer. After called, a new event is emitted holding the offer. The owner cannot invoke this function since he does not swap his own house.
Accept an offer
If the contract owner reads an offer and wants to accept it, he can call acceptOffer method. This method will change contract status to ACCEPTED and the contract will not accept more offers. Only contract owner can call this function.
Confirm swap
After the contract owner accepts an offer, the targetUser can confirm the swap by calling confirmSwap function. This function has some conditions:
If the offer specifies that the owner must pay some amount to the target, the owner will have to deposit enough ether so that the contract can transfer the required funds to the target. To do it, the owner can invoke the deposit function which is payable.
The same applies to the target. If the offer specifies that the target must pay some amount to the owner, the target will also have to deposit enough ether.
This function finishes changing the propietary of each house.
The contract code
Now we have seen an overview about how the contract works, let's dive into the code.
Events and storage vars
event NewOffer(Offer offer);
event BalanceUpdated(address, uint256);
address payable owner;
address payable targetUser;
Swap swap;
Statuses status;
mapping (address => uint256) balances;
As we can see, these first lines declare two events and five variables. Let's describe them one by one:
- event NewOffer: Function addOffer emits this event after a user sends an offer. An Offer type variable is passed to the event (We will see the Offer type in the next section).
- event BalanceUpdated(address, uint256): Function deposit emits this event when owner or targetUser deposits funds to the contract address. This is necessary when the swap requires transferring an extra payment to origin or target. BalanceUpdated receives address which updates its balance and the amount.
- address payable owner: This variable stores the owner's address. The one who deploys the contract and wants to swap his house. This address needs to be payable so that contract can transfer funds to it if required.
- address payable targetUser: This variable stores the address of the user that made the accepted offer. This address also needs to be payable so that contract can transfer funds to it if required
- Swap swap: This variable stores the swap information (we will see Swap struct in the next section).
- Statuses status: This variable stores swap status. We will see the possible status in the next section where we will see the Statuses enum.
- mapping (address => uint256) balances: This mapping stores balances both from owner and target. It is necessary to transfer funds if swap requires it.
Structs and enums
enum Statuses {
PENDING,
INITIALIZED,
ACCEPTED,
FINISHED
}
struct Swap {
House origin;
House target;
uint256 amountPayOriginToTarget;
uint256 amountPayTargetToOrigin;
}
struct House {
string houseType;
uint value;
string link;
address propietary;
}
struct Offer {
House house;
address targetUser;
uint256 amountPayOriginToTarget;
uint256 amountPayTargetToOrigin;
}
The contract declares some structs which includes common information, and an enum . Let's see each of them:
- enum Statuses: Stores the possible status of the contract.
-
struct House: Includes information about a house. This struct contains the following keys:
- houseType: House type can be duplex, apartment etc
- uint value: The value of the house. It can be the value in fiat or crypto. It does not matter since it is not used as a value to transfer.
- string link: The site where users can search for more information about the house
- address propietary : The address of the user who mades the offer or the owner address if the struct stores the owner house information.
-
struct Offer: This struct stores an offer made by a user who wants to swap. This struct contains the following keys:
- House house: The house that the user offers for swapping.
- address targetUser: The address of the user who sends the offer.
- uint256 extraPayOriginToTarget: A value greater than 0 indicates that owner has to pay an extra amount to the target user.
- uint256 extraPayTargetToOrigin: A value greater than 0 indicates that target user has to pay an extra amount to the owner.
-
struct Swap: This struct contains the swap data:
- House origin: Stores the owner house.
- House target: Stores the target house (The house from the offer accepted by the owner).
- uint256 amountPayOriginToTarget: This value is copied from accepted offer.
- uint256 amountPayTargetToOrigin: This value is also copied from accepted offer.
The constructor
constructor() {
owner = payable(msg.sender);
status = Statuses.PENDING;
}
The constructor is really simple. It sets the sender address (who deploys the contract) as the owner and sets the status as PENDING. Notice we convert sender address to payable so contract can transfer funds to it.
Modifiers
modifier hasToBeInitialized {
require(keccak256(abi.encodePacked(status)) == keccak256(abi.encodePacked(Statuses.INITIALIZED)), 'An offer has been already accepted or contract has not been initialized');
_;
}
modifier isOwner {
require(msg.sender == owner, 'Required contract owner');
_;
}
The contract creates two modifiers which will be used later:
- modifier hasToBeInitialized: Requires contract to be initialized, that is, variable status requires to be equal to INITIALIZED status.
- modifier isOwner: Requires sender to be the contract owner.
Initialize contract
function initialize(House memory house ) external isOwner {
house.propietary = owner;
swap.origin = house;
status = Statuses.INITIALIZED;
}
The initialize function stores the house which owner wants to swap and marks the status as INITIALIZED. This function uses isOwner modifier so only contract owner can invoke it.
Add Offers and confirm one
function addOffer(House memory house, uint256 amountPayOriginToTarget, uint256 amountPayTargetToOrigin) external hasToBeInitialized {
Offer memory offer = Offer(house, msg.sender, amountPayOriginToTarget, amountPayTargetToOrigin );
emit NewOffer(offer);
}
function acceptOffer(address payable _targetUser, House memory house, uint256 amountPayOriginToTarget, uint256 amountPayTargetToOrigin) external hasToBeInitialized isOwner
{
targetUser = _targetUser;
House memory targetHouse = house;
swap.target = targetHouse;
swap.amountPayOriginToTarget = amountPayOriginToTarget;
swap.amountPayTargetToOrigin = amountPayTargetToOrigin;
status = Statuses.ACCEPTED;
}
The addOffer function emits a NewOffer event with the offer data sent by a user interested on swapping. It uses the Offer struct to bundle offer data. This function requires the contract to be initialized (uses hasToBeInitialized modifier).
The acceptOffer function accepts an offer. It sets as targetUser the address received as first parameter. It also sets the accepted offers' data to the swap variable. Finally, it sets contract status as ACCEPTED.
Deposit
function deposit() external payable {
if(swap.amountPayOriginToTarget > 0){
require(msg.sender == owner, 'Origin must deposit enougth funds');
}
if(swap.amountPayTargetToOrigin > 0){
require(msg.sender == targetUser, 'Target must deposit enougth funds');
}
balances[msg.sender] = msg.value;
emit BalanceUpdated(msg.sender, msg.value);
}
The deposit function is a payable function. Both owner and targetUser can call it to deposit funds in the contract so that contract can transfer those funds later. As we saw at the begining of the article, the contract holds a mapping variable to store the balances. This function assigns the balance to the corresponding address and emits a BalanceUpdated event. Before doing this, it checks the following conditions:
- If amountPayOriginToTarget is greater that 0 then it requires owner to be the sender.
- If amountPayTargetToOrigin is greater that 0 then it requires targetUser to be the sender.
Performs swap
function performSwap() external
{
require(targetUser == msg.sender, 'Only target user can confirm swap');
require(keccak256(abi.encodePacked(status)) == keccak256(abi.encodePacked(Statuses.ACCEPTED)), 'An offer has not been accepted yet');
if(swap.amountPayOriginToTarget > 0) {
bool success = sendTransfer(owner, swap.amountPayOriginToTarget);
require(success, "Transfer to target failed.");
}
if(swap.amountPayTargetToOrigin > 0) {
bool success = sendTransfer(targetUser, swap.amountPayTargetToOrigin);
require(success, "Transfer to owner failed.");
}
swap.origin.propietary = targetUser;
swap.target.propietary = owner;
status = Statuses.FINISHED;
}
function sendTransfer(address payable addr, uint256 amount ) private returns (bool){
uint256 etherBalance = balances[addr] / 1 ether;
require(etherBalance >= amount, 'Deposit has not been sent or is lower than required' );
(bool success, ) = addr.call{value: amount}("");
return success;
}
The performSwap function transfers funds from origin to target if swap.amountPayOriginToTarget is greater than 0 or from target to origin if swap.amountPayTargetToOrigin is greater than 0.
The private function sendTransfer receives the payable address to send funds to and the amount to send. As balances are stored on wei, function first converts it to ether and the compares with the amount to transfer. If there is no enougth balance, function will revert.
After sending transfer, performSwap sets targetUser as origin propietary and owner as target propietary. Finally, it sets the contract status as FINISHED.
As we can see at the first two lines, this function requires contract to hold an ACCEPTED status and to be targetUser who invokes it.
Get Swap and status info
function getStatus() public view returns (Statuses) {
return status;
}
function info() public view hasToBeInitialized returns (Swap memory) {
return swap;
}
The getStatus function is a view function (does not modify the contract status) which simply returns the current status.
The info function returns the swap data when the contract status is INITIALIZED. This allows users to query the swap info and make offers.
Conclusion
This first part of the article has shown how to create a solidity contract for managing a house swap. In the next part we will learn how to compile and test our contract using the hardhat utilities.