Compiling and Testing Contracts
The three Solidity contracts that form the RealEstateToken project are set out in full here:
Whitelist - manages the Whitelist of approved investors.
RealEstateFactory - deploys RealEstateToken contracts.
RealEstateToken - Tokenization of a real estate development project.
Compiling and testing these contracts is simple. Foundry with it's Solidity centric test scripts, and builting helpers and assertions, make it straightforward to write unit tests for our contracts. The example project has a basic suite of tests for each contract.
Building
To build the Solidity contracts are built simply with:
forge buildTesting
Foundry unit tests are found in the test directory. There are a number of ways in which you can structure your tests. For simplicity, we've written one test script per contract. Test scripts are written in Solidity and the filename must have the form FileName.t.sol. You can read more about testing with Foundry here: https://getfoundry.sh/forge/tests/overview
You can run your unit tests in Foundry with a simple:
forge testThis will run all test scripts int the test directory.
The sample project includes the following tests scripts:
Whitelist.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import {Whitelist} from "../src/Whitelist.sol";
contract WhitelistTest is Test {
Whitelist wl;
address INVESTOR_A = vm.addr(1);
address INVESTOR_B = vm.addr(2);
address PROJECT_A = address(0xA1);
address PROJECT_B = address(0xB2);
event Approved(address indexed project, address indexed investor);
event Revoked(address indexed project, address indexed investor);
function setUp() public {
wl = new Whitelist();
}
function test_defaultFalse() public view {
assertFalse(wl.isApproved(PROJECT_A, INVESTOR_A));
assertFalse(wl.isApproved(PROJECT_A, INVESTOR_B));
}
function test_approve() public {
vm.expectEmit(true, true, false, false);
emit Approved(PROJECT_A, INVESTOR_A);
wl.approve(PROJECT_A, INVESTOR_A);
assertTrue(wl.isApproved(PROJECT_A, INVESTOR_A));
}
function test_revoke() public {
wl.approve(PROJECT_A, INVESTOR_A);
vm.expectEmit(true, true, false, false);
emit Revoked(PROJECT_A, INVESTOR_A);
wl.revoke(PROJECT_A, INVESTOR_A);
assertFalse(wl.isApproved(PROJECT_A, INVESTOR_A));
}
}RealEstateFactory.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import {RealEstateFactory} from "../src/RealEstateFactory.sol";
import {RealEstateToken} from "../src/RealEstateToken.sol";
interface IWhiteList {
function isApproved(address, address) external view returns (bool);
}
contract MockWhitelistAlwaysTrue is IWhiteList {
function isApproved(address, address) external pure returns (bool) {
return true;
}
}
contract RealEstateFactoryTest is Test {
RealEstateFactory factory;
MockWhitelistAlwaysTrue wl;
address INVESTOR_A = vm.addr(1); // used as issuer in one test
address INVESTOR_B = vm.addr(2);
string NAME = "Project A";
string SYMBOL = "PJA";
string PROPERTY_ID = "PJA-001";
string JURIS = "AU";
string META_URI = "ipfs://cid-a";
function setUp() public {
wl = new MockWhitelistAlwaysTrue();
factory = new RealEstateFactory(address(wl));
}
function test_constructor() public view {
assertEq(factory.whitelist(), address(wl));
assertEq(factory.owner(), address(this));
}
function test_deployProject() public {
address tokenAddr = factory.deployProject(
NAME,
SYMBOL,
PROPERTY_ID,
JURIS,
META_URI
);
assertTrue(tokenAddr != address(0));
address[] memory all = factory.getAllProjects();
assertEq(all.length, 1);
assertEq(all[0], tokenAddr);
RealEstateToken t = RealEstateToken(tokenAddr);
assertEq(t.name(), NAME);
assertEq(t.symbol(), SYMBOL);
assertEq(t.propertyId(), PROPERTY_ID);
assertEq(t.jurisdiction(), JURIS);
assertEq(t.metadataUri(), META_URI);
assertEq(address(t.whitelist()), address(wl));
assertEq(t.owner(), address(this));
}
function test_multipleDeployers() public {
vm.prank(INVESTOR_A);
address a = factory.deployProject("A", "A", "A", "AU", "ipfs://a");
vm.prank(INVESTOR_B);
address b = factory.deployProject("B", "B", "B", "AU", "ipfs://b");
address[] memory all = factory.getAllProjects();
assertEq(all.length, 2);
assertEq(RealEstateToken(a).owner(), INVESTOR_A);
assertEq(RealEstateToken(b).owner(), INVESTOR_B);
}
}RealEstateToken.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import {RealEstateToken} from "../src/RealEstateToken.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
interface IWhiteList {
function isApproved(
address project,
address investor
) external view returns (bool);
}
contract MockWhitelist is IWhiteList {
mapping(address => mapping(address => bool)) public approved;
function set(address project, address investor, bool ok) external {
approved[project][investor] = ok;
}
function isApproved(
address project,
address investor
) external view returns (bool) {
return approved[project][investor];
}
}
contract RealEstateTokenTest is Test {
MockWhitelist wl;
RealEstateToken token;
address INVESTOR_A = vm.addr(1);
address INVESTOR_B = vm.addr(2);
string NAME = "Project A";
string SYMBOL = "PJA";
string PROPERTY_ID = "PJA-001";
string JURIS = "AU";
string META_URI = "ipfs://cid-a";
function setUp() public {
wl = new MockWhitelist();
token = new RealEstateToken(
NAME,
SYMBOL,
address(wl),
PROPERTY_ID,
JURIS,
META_URI,
address(this) // project owner
);
}
function test_metadata() public view {
assertEq(token.name(), NAME);
assertEq(token.symbol(), SYMBOL);
assertEq(token.propertyId(), PROPERTY_ID);
assertEq(token.jurisdiction(), JURIS);
assertEq(token.metadataUri(), META_URI);
assertEq(token.decimals(), 0);
assertEq(token.MAX_SUPPLY(), 100);
assertEq(address(token.whitelist()), address(wl));
assertEq(token.owner(), address(this));
}
function test_mintWhenApproved() public {
wl.set(address(token), INVESTOR_A, true);
token.mint(INVESTOR_A, 10);
assertEq(token.totalSupply(), 10);
assertEq(token.balanceOf(INVESTOR_A), 10);
assertTrue(token.isShareholder(INVESTOR_A));
}
function test_mintWhenNotApprovedReverts() public {
vm.expectRevert(bytes("Investor not approved for this project."));
token.mint(INVESTOR_A, 1);
}
function test_transferRequiresWhitelist() public {
wl.set(address(token), INVESTOR_A, true);
wl.set(address(token), INVESTOR_B, true);
token.mint(INVESTOR_A, 5);
vm.prank(INVESTOR_A);
token.transfer(INVESTOR_B, 3);
assertEq(token.balanceOf(INVESTOR_A), 2);
assertEq(token.balanceOf(INVESTOR_B), 3);
assertTrue(token.isShareholder(INVESTOR_B));
}
function test_transferToUnapprovedReverts() public {
wl.set(address(token), INVESTOR_A, true);
token.mint(INVESTOR_A, 1);
// INVESTOR_B not approved
vm.prank(INVESTOR_A);
vm.expectRevert(bytes("Recipient is not approved for this project"));
token.transfer(INVESTOR_B, 1);
}
function test_adminBurnOnlyOwner() public {
wl.set(address(token), INVESTOR_A, true);
token.mint(INVESTOR_A, 4);
// non-owner cannot call adminBurn
vm.prank(INVESTOR_A);
vm.expectRevert(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector,
INVESTOR_A
)
);
token.adminBurn(INVESTOR_A, 1);
// owner can burn
token.adminBurn(INVESTOR_A, 2);
assertEq(token.totalSupply(), 2);
assertEq(token.balanceOf(INVESTOR_A), 2);
}
}Last updated