Testing IndonesiaPropertyToken
๐ฏ Tujuanโ
Setelah menyelesaikan bagian ini, Anda akan:
- โ Memahami pentingnya testing untuk RWA smart contracts
- โ Menulis unit tests untuk KYCRegistry
- โ Menulis unit tests untuk IndonesiaPropertyToken
- โ Menulis integration tests (2 contracts bekerja bersama)
- โ Menggunakan Foundry cheatcodes (vm.prank, vm.warp, vm.expectRevert)
- โ Mengimplementasikan fuzz testing untuk compliance checks
- โ Mencapai 100% test coverage
๐ก Kenapa Testing RWA Contracts Sangat Penting?โ
RWA = Real Money + Real Legal Consequencesโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ RWA Testing Priority โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Regular DeFi:
โ Bug = Loss of funds
โ
Can be fixed with new deployment
RWA Tokens:
โ Bug = Loss of funds
โ Bug = Legal liability
โ Bug = Regulatory violations
โ Bug = Investor lawsuits
โ Cannot easily redeploy (tokens already distributed!)
Testing Priority:
1. ๐ Access Control - Siapa bisa apa?
2. ๐ Compliance - KYC checks berfungsi?
3. ๐ซ Freeze/Unfreeze - AML controls work?
4. ๐ฐ Transfer Logic - Investment limits enforced?
5. ๐ Integration - 2 contracts interact correctly?
๐ Setup Test Filesโ
Project Structureโ
property-token-foundry/
โโโ src/
โ โโโ KYCRegistry.sol
โ โโโ IndonesiaPropertyToken.sol
โโโ test/
โ โโโ KYCRegistry.t.sol # โ Buat file ini
โ โโโ IndonesiaPropertyToken.t.sol # โ Buat file ini
โโโ foundry.toml
๐งช Test File 1: KYCRegistry.t.solโ
Buat file test/KYCRegistry.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import {KYCRegistry} from "../src/KYCRegistry.sol";
contract KYCRegistryTest is Test {
KYCRegistry public kyc;
// Test accounts
address public admin;
address public investor1;
address public investor2;
address public nonAdmin;
// Test constants
uint16 public constant INDONESIA = 360;
uint16 public constant SINGAPORE = 702;
uint256 public constant VALID_DAYS = 365;
// ============================================
// SETUP
// ============================================
function setUp() public {
admin = address(this);
investor1 = makeAddr("investor1");
investor2 = makeAddr("investor2");
nonAdmin = makeAddr("nonAdmin");
// Deploy KYCRegistry
kyc = new KYCRegistry();
}
// ============================================
// CONSTRUCTOR TESTS
// ============================================
function test_ConstructorSetsAdmin() public view {
assertEq(kyc.admin(), admin);
}
function test_InitialTotalInvestorsIsZero() public view {
assertEq(kyc.totalInvestors(), 0);
}
// ============================================
// REGISTER INVESTOR TESTS
// ============================================
function test_RegisterInvestorBasic() public {
kyc.registerInvestor(
investor1,
KYCRegistry.KYCLevel.BASIC,
INDONESIA,
VALID_DAYS
);
// Check investor registered
assertEq(kyc.totalInvestors(), 1);
assertTrue(kyc.isVerified(investor1));
// Check investor details
(
KYCRegistry.KYCLevel level,
uint256 expiryDate,
uint16 countryCode,
bool isActive
) = kyc.getInvestor(investor1);
assertEq(uint8(level), uint8(KYCRegistry.KYCLevel.BASIC));
assertEq(countryCode, INDONESIA);
assertTrue(isActive);
assertGt(expiryDate, block.timestamp);
}
function test_RegisterInvestorVerified() public {
kyc.registerInvestor(
investor1,
KYCRegistry.KYCLevel.VERIFIED,
INDONESIA,
VALID_DAYS
);
(KYCRegistry.KYCLevel level,,,) = kyc.getInvestor(investor1);
assertEq(uint8(level), uint8(KYCRegistry.KYCLevel.VERIFIED));
}
function test_RegisterInvestorAccredited() public {
kyc.registerInvestor(
investor1,
KYCRegistry.KYCLevel.ACCREDITED,
INDONESIA,
VALID_DAYS
);
(KYCRegistry.KYCLevel level,,,) = kyc.getInvestor(investor1);
assertEq(uint8(level), uint8(KYCRegistry.KYCLevel.ACCREDITED));
}
function test_RegisterInvestorEmitsEvent() public {
vm.expectEmit(true, false, false, true);
emit KYCRegistry.InvestorRegistered(investor1, KYCRegistry.KYCLevel.BASIC);
kyc.registerInvestor(
investor1,
KYCRegistry.KYCLevel.BASIC,
INDONESIA,
VALID_DAYS
);
}
function test_RegisterMultipleInvestors() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
kyc.registerInvestor(investor2, KYCRegistry.KYCLevel.VERIFIED, SINGAPORE, VALID_DAYS);
assertEq(kyc.totalInvestors(), 2);
assertTrue(kyc.isVerified(investor1));
assertTrue(kyc.isVerified(investor2));
}
// ============================================
// REGISTER INVESTOR REVERT TESTS
// ============================================
function test_RevertWhen_NonAdminRegisters() public {
vm.prank(nonAdmin);
vm.expectRevert("Only admin");
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
}
function test_RevertWhen_RegisterZeroAddress() public {
vm.expectRevert("Invalid address");
kyc.registerInvestor(address(0), KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
}
function test_RevertWhen_RegisterWithNoneLevel() public {
vm.expectRevert("Invalid KYC level");
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.NONE, INDONESIA, VALID_DAYS);
}
function test_RevertWhen_RegisterAlreadyRegistered() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
vm.expectRevert("Already registered");
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.VERIFIED, INDONESIA, VALID_DAYS);
}
// ============================================
// UPDATE INVESTOR TESTS
// ============================================
function test_UpdateInvestorLevel() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
kyc.updateInvestor(investor1, KYCRegistry.KYCLevel.VERIFIED);
(KYCRegistry.KYCLevel level,,,) = kyc.getInvestor(investor1);
assertEq(uint8(level), uint8(KYCRegistry.KYCLevel.VERIFIED));
}
function test_UpdateInvestorEmitsEvent() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
vm.expectEmit(true, false, false, true);
emit KYCRegistry.InvestorUpdated(investor1, KYCRegistry.KYCLevel.ACCREDITED);
kyc.updateInvestor(investor1, KYCRegistry.KYCLevel.ACCREDITED);
}
function test_RevertWhen_UpdateNonRegistered() public {
vm.expectRevert("Not registered");
kyc.updateInvestor(investor1, KYCRegistry.KYCLevel.VERIFIED);
}
function test_RevertWhen_NonAdminUpdates() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
vm.prank(nonAdmin);
vm.expectRevert("Only admin");
kyc.updateInvestor(investor1, KYCRegistry.KYCLevel.VERIFIED);
}
// ============================================
// REVOKE INVESTOR TESTS
// ============================================
function test_RevokeInvestor() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
kyc.revokeInvestor(investor1);
assertFalse(kyc.isVerified(investor1));
assertEq(kyc.totalInvestors(), 0);
(,,, bool isActive) = kyc.getInvestor(investor1);
assertFalse(isActive);
}
function test_RevokeInvestorEmitsEvent() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
vm.expectEmit(true, false, false, false);
emit KYCRegistry.InvestorRevoked(investor1);
kyc.revokeInvestor(investor1);
}
function test_RevertWhen_RevokeNonRegistered() public {
vm.expectRevert("Not registered");
kyc.revokeInvestor(investor1);
}
function test_RevertWhen_NonAdminRevokes() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
vm.prank(nonAdmin);
vm.expectRevert("Only admin");
kyc.revokeInvestor(investor1);
}
// ============================================
// IS VERIFIED TESTS
// ============================================
function test_IsVerifiedReturnsTrueForActiveInvestor() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
assertTrue(kyc.isVerified(investor1));
}
function test_IsVerifiedReturnsFalseForNonRegistered() public view {
assertFalse(kyc.isVerified(investor1));
}
function test_IsVerifiedReturnsFalseForRevokedInvestor() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
kyc.revokeInvestor(investor1);
assertFalse(kyc.isVerified(investor1));
}
function test_IsVerifiedReturnsFalseAfterExpiry() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
// Warp time past expiry
vm.warp(block.timestamp + VALID_DAYS * 1 days + 1);
assertFalse(kyc.isVerified(investor1));
}
// ============================================
// MEETS LEVEL TESTS
// ============================================
function test_MeetsLevelBasic() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
assertTrue(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.BASIC));
assertFalse(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.VERIFIED));
assertFalse(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.ACCREDITED));
}
function test_MeetsLevelVerified() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.VERIFIED, INDONESIA, VALID_DAYS);
assertTrue(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.BASIC));
assertTrue(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.VERIFIED));
assertFalse(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.ACCREDITED));
}
function test_MeetsLevelAccredited() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.ACCREDITED, INDONESIA, VALID_DAYS);
assertTrue(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.BASIC));
assertTrue(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.VERIFIED));
assertTrue(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.ACCREDITED));
}
function test_MeetsLevelReturnsFalseForExpired() public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.ACCREDITED, INDONESIA, VALID_DAYS);
vm.warp(block.timestamp + VALID_DAYS * 1 days + 1);
assertFalse(kyc.meetsLevel(investor1, KYCRegistry.KYCLevel.BASIC));
}
// ============================================
// FUZZ TESTS
// ============================================
function testFuzz_RegisterInvestorValidDays(uint256 validDays) public {
// Bound to reasonable range (1 day to 10 years)
validDays = bound(validDays, 1, 3650);
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, validDays);
(, uint256 expiryDate,,) = kyc.getInvestor(investor1);
assertEq(expiryDate, block.timestamp + (validDays * 1 days));
}
function testFuzz_RegisterInvestorCountryCode(uint16 countryCode) public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, countryCode, VALID_DAYS);
(, , uint16 savedCountryCode,) = kyc.getInvestor(investor1);
assertEq(savedCountryCode, countryCode);
}
function testFuzz_ExpiryTime(uint256 timeElapsed) public {
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
// Bound time to test range
timeElapsed = bound(timeElapsed, 0, VALID_DAYS * 1 days * 2);
vm.warp(block.timestamp + timeElapsed);
// Should be verified only if before expiry
bool expectedVerified = timeElapsed <= VALID_DAYS * 1 days;
assertEq(kyc.isVerified(investor1), expectedVerified);
}
}
๐งช Test File 2: IndonesiaPropertyToken.t.solโ
Buat file test/IndonesiaPropertyToken.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import {KYCRegistry} from "../src/KYCRegistry.sol";
import {IndonesiaPropertyToken} from "../src/IndonesiaPropertyToken.sol";
contract IndonesiaPropertyTokenTest is Test {
KYCRegistry public kyc;
IndonesiaPropertyToken public token;
// Test accounts
address public admin;
address public investor1;
address public investor2;
address public unverifiedUser;
// Property constants
string public constant TOKEN_NAME = "Sudirman Tower Token";
string public constant TOKEN_SYMBOL = "SDMN";
string public constant PROPERTY_NAME = "Apartemen Sudirman Tower";
string public constant LOCATION = "Jakarta Selatan";
uint256 public constant TOTAL_VALUE = 50_000_000_000; // Rp 50 Miliar
uint256 public constant TOTAL_TOKENS = 10000 ether; // 10,000 tokens
// KYC constants
uint16 public constant INDONESIA = 360;
uint256 public constant VALID_DAYS = 365;
// ============================================
// SETUP
// ============================================
function setUp() public {
admin = address(this);
investor1 = makeAddr("investor1");
investor2 = makeAddr("investor2");
unverifiedUser = makeAddr("unverifiedUser");
// Deploy KYCRegistry first
kyc = new KYCRegistry();
// Deploy PropertyToken with KYCRegistry address
token = new IndonesiaPropertyToken(
TOKEN_NAME,
TOKEN_SYMBOL,
address(kyc),
PROPERTY_NAME,
LOCATION,
TOTAL_VALUE,
TOTAL_TOKENS
);
// Register investors for KYC
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.VERIFIED, INDONESIA, VALID_DAYS);
kyc.registerInvestor(investor2, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
}
// ============================================
// CONSTRUCTOR TESTS
// ============================================
function test_ConstructorSetsTokenMetadata() public view {
assertEq(token.name(), TOKEN_NAME);
assertEq(token.symbol(), TOKEN_SYMBOL);
assertEq(token.decimals(), 18);
}
function test_ConstructorSetsPropertyInfo() public view {
(
string memory propertyName,
string memory location,
uint256 totalValue,
uint256 totalTokens,
,
bool isActive
) = token.property();
assertEq(propertyName, PROPERTY_NAME);
assertEq(location, LOCATION);
assertEq(totalValue, TOTAL_VALUE);
assertEq(totalTokens, TOTAL_TOKENS);
assertTrue(isActive);
}
function test_ConstructorMintsTokensToAdmin() public view {
assertEq(token.totalSupply(), TOTAL_TOKENS);
assertEq(token.balanceOf(admin), TOTAL_TOKENS);
}
function test_ConstructorSetsKYCRegistry() public view {
assertEq(token.kycRegistry(), address(kyc));
}
function test_RevertWhen_ConstructorWithZeroKYCRegistry() public {
vm.expectRevert("Invalid KYC registry");
new IndonesiaPropertyToken(
TOKEN_NAME,
TOKEN_SYMBOL,
address(0), // Invalid
PROPERTY_NAME,
LOCATION,
TOTAL_VALUE,
TOTAL_TOKENS
);
}
// ============================================
// TRANSFER TESTS (COMPLIANCE)
// ============================================
function test_TransferToVerifiedInvestor() public {
uint256 transferAmount = 100 ether;
// Admin transfers to verified investor
token.transfer(investor1, transferAmount);
assertEq(token.balanceOf(investor1), transferAmount);
assertEq(token.balanceOf(admin), TOTAL_TOKENS - transferAmount);
}
function test_TransferEmitsEvent() public {
uint256 transferAmount = 100 ether;
vm.expectEmit(true, true, false, true);
emit IndonesiaPropertyToken.Transfer(admin, investor1, transferAmount);
token.transfer(investor1, transferAmount);
}
function test_TransferBetweenVerifiedInvestors() public {
// Admin sends to investor1
token.transfer(investor1, 500 ether);
// Investor1 sends to investor2
vm.prank(investor1);
token.transfer(investor2, 200 ether);
assertEq(token.balanceOf(investor1), 300 ether);
assertEq(token.balanceOf(investor2), 200 ether);
}
function test_RevertWhen_TransferToUnverifiedUser() public {
vm.expectRevert("Not KYC verified");
token.transfer(unverifiedUser, 100 ether);
}
function test_RevertWhen_TransferFromUnverifiedUser() public {
// First, force transfer some tokens to unverified user (admin can do this)
token.forceTransfer(admin, unverifiedUser, 100 ether);
// Unverified user tries to transfer
vm.prank(unverifiedUser);
vm.expectRevert("Not KYC verified");
token.transfer(investor1, 50 ether);
}
function test_RevertWhen_TransferExceedsBalance() public {
vm.prank(investor1);
vm.expectRevert("Insufficient balance");
token.transfer(investor2, 100 ether);
}
function test_RevertWhen_TransferToZeroAddress() public {
vm.expectRevert("Invalid recipient");
token.transfer(address(0), 100 ether);
}
function test_RevertWhen_TransferExceedsMaxInvestment() public {
// Max investment is 1000 ether by default
// Try to transfer more than max
vm.expectRevert("Exceeds max investment");
token.transfer(investor1, 1001 ether);
}
// ============================================
// TRANSFER FROM TESTS
// ============================================
function test_TransferFrom() public {
// Admin approves investor1 to spend tokens
token.approve(investor1, 500 ether);
// Investor1 transfers from admin to investor2
vm.prank(investor1);
token.transferFrom(admin, investor2, 200 ether);
assertEq(token.balanceOf(investor2), 200 ether);
assertEq(token.allowance(admin, investor1), 300 ether);
}
function test_RevertWhen_TransferFromInsufficientAllowance() public {
token.approve(investor1, 100 ether);
vm.prank(investor1);
vm.expectRevert("Insufficient allowance");
token.transferFrom(admin, investor2, 200 ether);
}
// ============================================
// APPROVE TESTS
// ============================================
function test_Approve() public {
token.approve(investor1, 500 ether);
assertEq(token.allowance(admin, investor1), 500 ether);
}
function test_ApproveEmitsEvent() public {
vm.expectEmit(true, true, false, true);
emit IndonesiaPropertyToken.Approval(admin, investor1, 500 ether);
token.approve(investor1, 500 ether);
}
// ============================================
// FREEZE ACCOUNT TESTS
// ============================================
function test_FreezeAccount() public {
token.freezeAccount(investor1, "Suspicious activity");
assertTrue(token.frozen(investor1));
}
function test_FreezeAccountEmitsEvent() public {
vm.expectEmit(true, false, false, true);
emit IndonesiaPropertyToken.AccountFrozen(investor1, "AML investigation");
token.freezeAccount(investor1, "AML investigation");
}
function test_RevertWhen_TransferFromFrozenAccount() public {
// Give investor1 some tokens
token.transfer(investor1, 500 ether);
// Freeze investor1
token.freezeAccount(investor1, "AML investigation");
// Investor1 tries to transfer
vm.prank(investor1);
vm.expectRevert("Account is frozen");
token.transfer(investor2, 100 ether);
}
function test_RevertWhen_TransferToFrozenAccount() public {
// Freeze investor1
token.freezeAccount(investor1, "AML investigation");
// Admin tries to transfer to frozen account
vm.expectRevert("Account is frozen");
token.transfer(investor1, 100 ether);
}
function test_RevertWhen_NonAdminFreezes() public {
vm.prank(investor1);
vm.expectRevert("Only admin");
token.freezeAccount(investor2, "Suspicious");
}
// ============================================
// UNFREEZE ACCOUNT TESTS
// ============================================
function test_UnfreezeAccount() public {
token.freezeAccount(investor1, "Investigation");
token.unfreezeAccount(investor1);
assertFalse(token.frozen(investor1));
}
function test_UnfreezeAccountEmitsEvent() public {
token.freezeAccount(investor1, "Investigation");
vm.expectEmit(true, false, false, false);
emit IndonesiaPropertyToken.AccountUnfrozen(investor1);
token.unfreezeAccount(investor1);
}
function test_TransferAfterUnfreeze() public {
// Give investor1 tokens
token.transfer(investor1, 500 ether);
// Freeze then unfreeze
token.freezeAccount(investor1, "Investigation");
token.unfreezeAccount(investor1);
// Should be able to transfer now
vm.prank(investor1);
token.transfer(investor2, 100 ether);
assertEq(token.balanceOf(investor2), 100 ether);
}
// ============================================
// FORCE TRANSFER TESTS
// ============================================
function test_ForceTransfer() public {
// Give investor1 tokens
token.transfer(investor1, 500 ether);
// Force transfer (even if frozen or unverified destination)
token.forceTransfer(investor1, unverifiedUser, 200 ether);
assertEq(token.balanceOf(investor1), 300 ether);
assertEq(token.balanceOf(unverifiedUser), 200 ether);
}
function test_ForceTransferFromFrozenAccount() public {
token.transfer(investor1, 500 ether);
token.freezeAccount(investor1, "Court order");
// Admin can still force transfer from frozen account
token.forceTransfer(investor1, investor2, 300 ether);
assertEq(token.balanceOf(investor2), 300 ether);
}
function test_ForceTransferEmitsEvent() public {
token.transfer(investor1, 500 ether);
vm.expectEmit(true, true, false, true);
emit IndonesiaPropertyToken.Transfer(investor1, investor2, 200 ether);
token.forceTransfer(investor1, investor2, 200 ether);
}
function test_RevertWhen_NonAdminForceTransfers() public {
token.transfer(investor1, 500 ether);
vm.prank(investor1);
vm.expectRevert("Only admin");
token.forceTransfer(investor1, investor2, 100 ether);
}
function test_RevertWhen_ForceTransferInsufficientBalance() public {
vm.expectRevert("Insufficient balance");
token.forceTransfer(investor1, investor2, 100 ether);
}
// ============================================
// SET LEGAL DOCUMENT TESTS
// ============================================
function test_SetLegalDocument() public {
string memory ipfsHash = "QmXyz123456789...";
token.setLegalDocument(ipfsHash);
(,,,, string memory legalDoc,) = token.property();
assertEq(legalDoc, ipfsHash);
}
function test_RevertWhen_NonAdminSetsLegalDocument() public {
vm.prank(investor1);
vm.expectRevert("Only admin");
token.setLegalDocument("QmXyz...");
}
// ============================================
// SET INVESTMENT LIMITS TESTS
// ============================================
function test_SetInvestmentLimits() public {
token.setInvestmentLimits(0.5 ether, 2000 ether);
assertEq(token.minInvestment(), 0.5 ether);
assertEq(token.maxInvestment(), 2000 ether);
}
function test_RevertWhen_InvalidLimits() public {
vm.expectRevert("Invalid limits");
token.setInvestmentLimits(1000 ether, 500 ether); // min > max
}
function test_RevertWhen_NonAdminSetsLimits() public {
vm.prank(investor1);
vm.expectRevert("Only admin");
token.setInvestmentLimits(1 ether, 500 ether);
}
// ============================================
// VIEW FUNCTIONS TESTS
// ============================================
function test_GetOwnershipPercent() public {
// Transfer 1000 tokens (10% of 10000)
token.transfer(investor1, 1000 ether);
uint256 ownershipBps = token.getOwnershipPercent(investor1);
assertEq(ownershipBps, 1000); // 1000 basis points = 10%
}
function test_GetOwnershipPercentAdmin() public view {
// Admin owns 100% initially
uint256 ownershipBps = token.getOwnershipPercent(admin);
assertEq(ownershipBps, 10000); // 10000 basis points = 100%
}
function test_GetTokenValueIDR() public view {
uint256 tokenValue = token.getTokenValueIDR();
// 50 Miliar / 10000 tokens = 5 Juta per token
assertEq(tokenValue, 5_000_000);
}
function test_CanTransferReturnsTrue() public view {
(bool canTransfer, string memory reason) = token.canTransfer(admin, investor1, 100 ether);
assertTrue(canTransfer);
assertEq(reason, "Transfer allowed");
}
function test_CanTransferReturnsFalseForFrozenSender() public {
token.transfer(investor1, 500 ether);
token.freezeAccount(investor1, "Investigation");
(bool canTransfer, string memory reason) = token.canTransfer(investor1, investor2, 100 ether);
assertFalse(canTransfer);
assertEq(reason, "Sender is frozen");
}
function test_CanTransferReturnsFalseForFrozenReceiver() public {
token.freezeAccount(investor1, "Investigation");
(bool canTransfer, string memory reason) = token.canTransfer(admin, investor1, 100 ether);
assertFalse(canTransfer);
assertEq(reason, "Receiver is frozen");
}
function test_CanTransferReturnsFalseForUnverifiedSender() public {
token.forceTransfer(admin, unverifiedUser, 100 ether);
(bool canTransfer, string memory reason) = token.canTransfer(unverifiedUser, investor1, 50 ether);
assertFalse(canTransfer);
assertEq(reason, "Sender not KYC verified");
}
function test_CanTransferReturnsFalseForUnverifiedReceiver() public {
(bool canTransfer, string memory reason) = token.canTransfer(admin, unverifiedUser, 100 ether);
assertFalse(canTransfer);
assertEq(reason, "Receiver not KYC verified");
}
function test_CanTransferReturnsFalseForInsufficientBalance() public {
(bool canTransfer, string memory reason) = token.canTransfer(investor1, investor2, 100 ether);
assertFalse(canTransfer);
assertEq(reason, "Insufficient balance");
}
function test_CanTransferReturnsFalseForExceedsMax() public {
(bool canTransfer, string memory reason) = token.canTransfer(admin, investor1, 1001 ether);
assertFalse(canTransfer);
assertEq(reason, "Exceeds max investment");
}
// ============================================
// INTEGRATION TESTS: KYC EXPIRY
// ============================================
function test_TransferFailsAfterKYCExpiry() public {
// Give investor1 some tokens
token.transfer(investor1, 500 ether);
// Warp time past KYC expiry
vm.warp(block.timestamp + VALID_DAYS * 1 days + 1);
// Should fail because KYC expired
vm.prank(investor1);
vm.expectRevert("Not KYC verified");
token.transfer(investor2, 100 ether);
}
function test_TransferSucceedsAfterKYCRenewal() public {
token.transfer(investor1, 500 ether);
// Warp time past expiry
vm.warp(block.timestamp + VALID_DAYS * 1 days + 1);
// Revoke and re-register (renewal) for both investors
kyc.revokeInvestor(investor1);
kyc.registerInvestor(investor1, KYCRegistry.KYCLevel.VERIFIED, INDONESIA, VALID_DAYS);
kyc.revokeInvestor(investor2);
kyc.registerInvestor(investor2, KYCRegistry.KYCLevel.BASIC, INDONESIA, VALID_DAYS);
// Should work now
vm.prank(investor1);
token.transfer(investor2, 100 ether);
assertEq(token.balanceOf(investor2), 100 ether);
}
// ============================================
// INTEGRATION TESTS: KYC REVOCATION
// ============================================
function test_TransferFailsAfterKYCRevoked() public {
token.transfer(investor1, 500 ether);
// Revoke KYC
kyc.revokeInvestor(investor1);
// Should fail
vm.prank(investor1);
vm.expectRevert("Not KYC verified");
token.transfer(investor2, 100 ether);
}
// ============================================
// FUZZ TESTS
// ============================================
function testFuzz_TransferAmount(uint256 amount) public {
// Bound amount to valid range (min 1 wei, max 1000 ether due to maxInvestment)
amount = bound(amount, 1, 1000 ether);
token.transfer(investor1, amount);
assertEq(token.balanceOf(investor1), amount);
assertEq(token.balanceOf(admin), TOTAL_TOKENS - amount);
}
function testFuzz_OwnershipPercent(uint256 balance) public {
balance = bound(balance, 1, 1000 ether);
token.transfer(investor1, balance);
uint256 ownershipBps = token.getOwnershipPercent(investor1);
uint256 expectedBps = (balance * 10000) / TOTAL_TOKENS;
assertEq(ownershipBps, expectedBps);
}
function testFuzz_MultipleTransfers(uint256 amount1, uint256 amount2) public {
// Bound amounts
amount1 = bound(amount1, 1, 500 ether);
amount2 = bound(amount2, 1, 500 ether);
// Transfer to investor1
token.transfer(investor1, amount1);
// Transfer to investor2
token.transfer(investor2, amount2);
assertEq(token.balanceOf(investor1), amount1);
assertEq(token.balanceOf(investor2), amount2);
assertEq(token.balanceOf(admin), TOTAL_TOKENS - amount1 - amount2);
}
}
๐ Running Testsโ
Run All Testsโ
forge test
Output yang diharapkan:
Ran 65 tests for test/IndonesiaPropertyToken.t.sol:IndonesiaPropertyTokenTest
[PASS] testFuzz_MultipleTransfers(uint256,uint256) (runs: 256)
[PASS] testFuzz_OwnershipPercent(uint256) (runs: 256)
[PASS] testFuzz_TransferAmount(uint256) (runs: 256)
[PASS] test_Approve() (gas: 45678)
[PASS] test_CanTransferReturnsTrue() (gas: 23456)
...
Ran 30 tests for test/KYCRegistry.t.sol:KYCRegistryTest
[PASS] testFuzz_ExpiryTime(uint256) (runs: 256)
[PASS] testFuzz_RegisterInvestorCountryCode(uint16) (runs: 256)
[PASS] testFuzz_RegisterInvestorValidDays(uint256) (runs: 256)
[PASS] test_ConstructorSetsAdmin() (gas: 12345)
...
Suite result: ok. 95 passed; 0 failed; finished in 3.21s
Run with Verbosityโ
forge test -vv # Show logs
forge test -vvvv # Show traces
Run Specific Test Fileโ
# Only KYCRegistry tests
forge test --match-path test/KYCRegistry.t.sol
# Only PropertyToken tests
forge test --match-path test/IndonesiaPropertyToken.t.sol
Run Specific Testโ
forge test --match-test test_TransferToVerifiedInvestor
Gas Reportโ
forge test --gas-report
Test Coverageโ
forge coverage
Output:
| File | % Lines | % Statements | % Branches | % Funcs |
|-----------------------------|-----------------|-----------------|----------------|----------------|
| src/KYCRegistry.sol | 100.00% (25/25) | 100.00% (30/30) | 100.00% (12/12)| 100.00% (8/8) |
| src/IndonesiaPropertyToken.sol| 100.00% (45/45)| 100.00% (55/55) | 100.00% (20/20)| 100.00% (18/18)|
โ 100% coverage!
๐ Testing Checklist untuk RWA Contractsโ
KYCRegistryโ
Admin Functions:
- registerInvestor - success & reverts
- updateInvestor - success & reverts
- revokeInvestor - success & reverts
View Functions:
- isVerified - all scenarios
- meetsLevel - all levels
- getInvestor - data integrity
Edge Cases:
- KYC expiry (time-based)
- Multiple investors
- Level hierarchy
IndonesiaPropertyTokenโ
ERC-20 Functions:
- transfer - with compliance
- transferFrom - with compliance
- approve - standard behavior
Compliance Functions:
- freezeAccount - AML control
- unfreezeAccount - recovery
- forceTransfer - legal compliance
Admin Functions:
- setLegalDocument - IPFS hash
- setInvestmentLimits - min/max
View Functions:
- getOwnershipPercent - calculation
- getTokenValueIDR - calculation
- canTransfer - pre-check
Integration Tests:
- KYC expiry affects transfers
- KYC revocation affects transfers
- Freeze + KYC interactions
๐ Ringkasanโ
Dalam modul ini, Anda telah mempelajari:
- Pentingnya Testing RWA Contracts - Legal & financial implications
- Unit Tests untuk KYCRegistry - All admin & view functions
- Unit Tests untuk IndonesiaPropertyToken - ERC-20 + compliance
- Integration Tests - 2 contracts working together
- Fuzz Testing - Random inputs untuk compliance checks
- 100% Test Coverage - Professional standard
Dengan testing yang comprehensive, Anda siap untuk deploy RWA tokens yang aman dan compliant!
Trust, but verify! ๐