Skip to main content
Create your own

Testing Compliance Reversion for Transfers

Welcome back! In our last lesson, you successfully tested the "happy path" for our SecurityToken, proving that compliant transfers between allowlisted investors work exactly as intended. This confirmed that the core positive functionality is sound.

Now, we turn our attention to an equally, if not more, critical aspect of security: ensuring the contract correctly rejects non-compliant actions. This lesson is dedicated to testing the "sad paths"—the scenarios where our transfer rules should be violated. You will learn how to write tests that verify your contract not only fails these transactions but fails them for the precise reasons you've defined. This practice is fundamental to building robust and truly secure tokenized assets.

1. Understanding Reverts: The Blockchain's "No"

Before we write the tests, let's solidify our understanding of what happens when a smart contract rule is broken. In Solidity, the primary mechanism for rejecting a transaction is the revert operation. When a condition in a require statement is not met, the transaction is reverted. This means two things:

  1. State Rollback: Any state changes made during the transaction are completely undone, as if the transaction never happened.
  2. Gas Consumption: The gas used up to the point of the revert is still consumed by the sender.

This atomic "all-or-nothing" execution is a cornerstone of blockchain security. A reverted transaction is marked as "Fail" on a block explorer, often with a message like "execution reverted."

An example of a failed ERC-20 token transfer on a block explorer. The "execution reverted" warning indicates that a `require` statement or another error condition was triggered, causing the transaction to fail and the state to be rolled back.

Our goal in testing is to intentionally trigger these reverts and confirm they happen exactly when and how we expect them to.

2. Testing for Reverts with Hardhat and Chai

To test for reverted transactions in our Hardhat environment, we use a special matcher provided by the Chai assertion library: revertedWith. This allows us to assert not only that a transaction reverted, but that it did so with a specific error message.

The basic syntax looks like this:

await expect(contract.someFunctionThatShouldFail())
    .to.be.revertedWith("Your specific error message");

This is incredibly powerful because it validates that our contract is failing for the correct reason. A generic revert could mask an unexpected bug, but checking the error string ensures our intended logic is what's protecting the contract.

The following video provides an excellent, practical walkthrough of writing tests for both successful and failing conditions, including the use of revertedWith.

Hardhat Testing Tutorial | Solidity Smart Contract Testing Developer | Hardhat Testing Course

This tutorial from Daulat Hussain on Hardhat testing clearly demonstrates how to handle different test scenarios. It covers testing for both expected outcomes and expected failures.

Please watch two key segments: First, focus on the section where a withdrawal is attempted before the time lock has expired. Observe how to.be.revertedWith is used to check for the correct error message in this test. Next, watch how another test is written to ensure a non-owner cannot withdraw funds, again using revertedWith to assert the failure in this part. This scenario is very similar to what we'll be testing with our allowlist.

3. Writing Tests for Non-Compliant Transfers

Armed with the revertedWith matcher, let's apply it to our SecurityToken. In the previous lesson, we created a nested describe("Transfers", ...) block. We will now add our new "sad path" tests within this same block, leveraging the beforeEach hook we already wrote.

Recall our setup: investor1 and investor2 are on the allowlist, but unauthorizedUser is not. investor1 holds 1000 tokens.

Test Case 1: Sender Allowlisted, Recipient Not

Our first test will simulate an allowlisted investor trying to send tokens to an address that is not on the allowlist. This must be blocked.

  • Arrange: The beforeEach hook has already set this up. investor1 has tokens and is allowlisted; unauthorizedUser is not.
  • Act: We connect as investor1 and attempt to transfer tokens to unauthorizedUser.
  • Assert: We expect the transaction to.be.revertedWith the specific error message from our _beforeTokenTransfer hook: "Compliance: recipient not allowlisted".

Here is the test code to add to your securityToken.test.js file, inside the describe("Transfers", ...) block:

it("should revert if recipient is not allowlisted", async function () {
    // Act & Assert
    await expect(
        securityToken.connect(investor1).transfer(unauthorizedUser.address, transferAmount)
    ).to.be.revertedWith("Compliance: recipient not allowlisted");
});

Test Case 2: Sender Not Allowlisted

Next, we must test the reverse scenario: an address that is not on the allowlist attempting to move tokens. This also must be blocked, even if they somehow received tokens (e.g., before the compliance rules were active).

  • Arrange: This requires a small, specific setup. We first use the minter account to mint some tokens directly to unauthorizedUser.
  • Act: We connect as unauthorizedUser and attempt to transfer tokens to investor1 (who is on the allowlist).
  • Assert: We expect this transaction to.be.revertedWith the error message "Compliance: sender not allowlisted".

Here is the second test to add:

it("should revert if sender is not allowlisted", async function () {
    // Arrange: Give tokens to a non-allowlisted user
    await securityToken.connect(minter).mint(unauthorizedUser.address, mintAmount);
    
    // Act & Assert
    await expect(
        securityToken.connect(unauthorizedUser).transfer(investor1.address, transferAmount)
    ).to.be.revertedWith("Compliance: sender not allowlisted");
});

This pattern of testing for expected reverts is a universal practice in smart contract development. The image below shows a similar test written in Foundry, a different testing framework. Notice the use of vm.expectRevert—it achieves the same goal, proving the concept is fundamental regardless of the specific tools.

A Solidity test written using the Foundry framework. The `vm.expectRevert("Not owner")` command serves the same purpose as Hardhat/Chai's `revertedWith`, asserting that the `privileged()` function call fails with the specified error message.

4. The Broader World of On-Chain Compliance

Our allowlist is a simple but powerful compliance tool. In the real world of tokenized securities, compliance can involve a complex web of rules. Your background in data warehousing and investment management software gives you a unique appreciation for such rule-based systems. On-chain compliance engines formalize these rules into immutable code.

The Capital Markets and Technology Association (CMTA) maintains a repository of standardized, modular compliance rules. This provides a fascinating glimpse into a production-grade compliance framework.

CMTA/Rules: Rules for CMTAT and RuleEngine - GitHub

This GitHub repository contains a collection of on-chain compliance rules for security tokens. It illustrates how a simple allowlist is just one piece of a much larger puzzle.

Please explore the following sections in the README.md file: First, review the table of restriction codes. Notice how each specific violation (e.g., sender not whitelisted, recipient not whitelisted) has a unique numerical code. This is analogous to error code systems in traditional software. Next, read the overview of Validation Rules. This lists different types of rules, such as RuleWhitelist, RuleBlacklist, and RuleSanctionList. Finally, read the brief descriptions for the Whitelist rule and the Blacklist rule. This will reinforce the logic we are currently testing.

This resource demonstrates that robust on-chain compliance is not about a single rule, but a system of interoperable rules that can be combined to meet complex regulatory requirements. The principles are the same as in traditional finance, but the enforcement is atomic and transparent.

Conclusion

In this lesson, you've learned how to test the critical failure scenarios of your smart contract. By ensuring your SecurityToken correctly reverts non-compliant transfers, you have significantly hardened its security and validated its core value proposition.

Key Takeaways:

  • A revert operation in Solidity stops execution, rolls back state changes, and is the primary tool for enforcing rules.
  • The revertedWith("error message") matcher is used in tests to assert that a transaction fails for the correct, specific reason.
  • Thorough testing requires covering both "happy paths" (expected success) and "sad paths" (expected failure).
  • Simple allowlists are just one component of a larger ecosystem of on-chain compliance rules that can enforce complex regulations.

You've now tested issuance and the full range of transfer logic. In our next session, we will build on this foundation by implementing and testing more advanced administrative functions, such as the ability to freeze accounts—a concept you saw in the CMTA RuleERC2980.

Can't find a good explanation? Sign up and we'll make it for you

Sign up