How to Test Ethereum Smart Contracts for Access Restriction
Write Solidity tests to ensure access to functions is correctly restricted

Prerequisite: Ths article assumes an understanding of Solidity and Ethereum smart contracts.
Controlling access to smart contracts is vital to ensuring security. Common patterns, like OpenZeppelin’s Ownable and AccessControl contracts, enable developers to easily implement various levels of access to functions.
It’s great using these libraries because they’re audited and reviewed by a large community of developers, minimising the risk and maximising their utility. But how do you know you’re using them correctly?
We all make mistakes, even the best of us. What if you miss an onlyOwner declaration in one of your functions? Without tests, you could be deploying code with a huge vulnerability.
How can we ensure that functions are restricted correctly?
Testing ‘OnlyOwner’ Access With Solidity
Let’s assume we have a contract called ExampleContract that implements OpenZeppelin’s Ownable contract. It has many functions, some of which can be called by any address and some of which are only executable from the owner of the contract. This is where the onlyOwner modifier from Ownable is used:
function somethingSomethingTheOwnerCanDo() public onlyOwner {
...
}If our contract has lots of functions, it’s very easy to miss one simple modifier. So we need to write tests for every successful path through the code and every unsuccessful path.
Truffle Suite
The Truffle Suite allows us to write tests in both JavaScript and Solidity. It’s easy to test the onlyOwner modifier exists using JavaScript tests because multiple accounts can be injected into the test runner. Send the message from the wrong account, and you’ll get a revert.
In Solidity tests, this is more difficult since all the tests are being run from a single address: the test contract. So how can we imitate this in Solidity tests?
The answer: By using a proxy contract
Using our somethingSomethingTheOwnerCanDo() function as the target of our test, we need a test that ensures it reverts if someone other than the owner calls it.
Let’s start by creating the ProxyContract, which makes the call that we expect to fail because it’s not the owner.
pragma solidity ^0.6.7;import "./ExampleContract.sol"contract ProxyContract {
function attemptNonOwnerCall(ExampleContract _contract) public {
_contract.somethingSomethingTheOwnerCanDo();
}
}In our tests, we need to initialize this ProxyContract and call attemptNonOwnerCall with the instance of ExampleContract being tested as the parameter. This test needs to assert the call will fail. We can do that with try-catch. Here’s our test:
function testNonOwnerFails() public {
ExampleContract testTarget = new ExampleContract();
ProxyContract proxy = new ProxyContract();
string expectedReason = "Owner: caller is not the owner";
try proxy.attemptNonOwnerCall(testTarget) {
revert("It should fail");
} catch Error(string memory reason) {
Assert.equal(reason, expectedReason, "It should fail");
}
}Let’s walk through this line by line:
testTargetis the contract we’re testing, with the restricted function- We create a new
proxycontract expectedReasonis the message thatOwnablereturns when a call violates theonlyOwnermodifier- We try to call the restricted function through our
proxy, who isn’t the owner, so we expect to catch an error - If there’s no error, the
revertcommand is run inside thetryblock, causing our test to fail - If there’s an error, the
catchblock is invoked, and we assert that it’s the error we were expecting:expectedReason
This only works because the owner of ExampleContract is our test contract (the contract which initialized ExampleContract), and the msg.sender of the call to somethingSomethingTheOwnerCanDo() is the ProxyContract, which has a different address.
Conclusion
It’s easy to assume that because we use well-tested libraries that our code is safe. While that’s a fair assumption, the only way to know you’re using them correctly is by thoroughly testing each function and ensuring your expectations of what they do line up with reality.
