TUTORIAL: Sophia basics – owning a smart contract and owner privileges
Tutorial Overview
This tutorial will teach you how one can access restrict a method to the deployer of the smart contract itself. This is a useful tool for defining critical business logic.
Prerequisites
- Installed aeproject – For installation steps refer to this tutorial.
1. Prepare your environment
Before we begin coding up we should start by initializing an æpp project.
aeproject
is a npm package and binary executable allowing you to easily setup your development environment, run local nodes, compile and run deploy and test scripts.
1.1 aeproject init
Let’s create a new empty folder and initialize an æpp project.
mkdir ownable-project
cd ownable-project
aeproject init
1.2 aeproject env
Another thing we would require is a local working environment to develop against.
We are going to use the one provided by aeproject
.
Just type in the following command:
aeproject env
You can later stop the local environment by typing in:
aeproject env --stop
2. Writing the contract without access restriction
The next thing that we will do is to write a simple Sophia smart contract. This smart contract will have two functions – public and access restricted. In this section, we will not apply an access restriction yet, but we will write unit tests proving that the soon to be access restricted method is still called successfully by anyone.
2.1 Write the smart contract
Let’s first write our Restricted.aes
smart contract.
First create the contract file under contracts folder:
touch ./contracts/Restricted.aes
touch
creates a new file. This file can be edited via your favorite editor.
The following code goes inside this file:
contract Restricted =
// Non access restricted method. Should be callable by any user successfully
entrypoint nonRestrictedFunction(i : int) : int =
i
// Access restricted method. Should be callable only by the owner/deployer
entrypoint restrictedFunction(i: int) : int =
i
Note: keep formatting intact or things will break badly
As you can see this contract exposes two functions – both returning the number it has received as an argument. Note that the restricted function is not actually restricted yet.
2.2 Write unit tests to call the methods
In order to show the functionality we want we should write some unit tests.
The main three unit tests needed are:
- The smart contract is being compiled and deployed successfully
- The non-restricted method is freely called by non-owner (more on owners soon)
- The restricted method cannot be called by non-owner
Add the following code to your test/exampleTest.js
file:
const Deployer = require('aeproject-lib').Deployer;
const RESTRICTED_CONTRACT_PATH = "./contracts/Restricted.aes";
describe('Restricted Contract', () => {
let owner;
let ownerKeyPair = wallets[0];
let nonOwnerKeyPair = wallets[1];
let ownerDeployedContract;
before(async () => {
// Create client object
owner = new Deployer('local', ownerKeyPair.secretKey);
})
it('Deploying Restricted Contract', async () => {
// Deploy it
ownerDeployedContract = owner.deploy(RESTRICTED_CONTRACT_PATH);
// See if it has deployed correctly
await assert.isFulfilled(ownerDeployedContract, 'Could not deploy the Restricted Smart Contract');
})
describe('Calling Functions', () => {
it('Should successfully call the non restricted method', async () => {
ownerDeployedContract = await owner.deploy(RESTRICTED_CONTRACT_PATH);
// Call the non restricted method
const callNotRestrictedMethod = ownerDeployedContract.nonRestrictedFunction(3);
await assert.isFulfilled(callNotRestrictedMethod, 'Calling the non restricted function failed');
const callResult = await callNotRestrictedMethod;
await assert.equal(callResult.decodedResult, 3, 'The returned data was not correct')
})
it('Should successfully call the restricted method', async () => {
const nonOwnerCalling = await ownerDeployedContract.from(nonOwnerKeyPair.secretKey);
// Call the restricted method from non owner
const callRestrictedMethod = nonOwnerCalling.restrictedFunction(2);
// It should be rejected as it is restricted to owner only
await assert.isRejected(callRestrictedMethod);
})
})
})
As we have not yet implemented the restriction functionality the third test naturally fails.
Run the following command in the terminal:
aeproject test
Output of aeproject test
:
===== Starting Tests =====
Restricted Contract
===== Contract: Restricted.aes has been deployed at ct_2JVfaVS61gCcMEWfstZRuj6yAnVTRyvSbVsfFAsvGrxm24b5fd =====
✓ Deploying Restricted Contract (8785ms)
Calling Functions
===== Contract: Restricted.aes has been deployed at ct_2XY9zP2ZgXExKkRqAKoot6gv1ttkcREu1d5LEtY5s9Jka5DqNy =====
✓ Should successfully call the non restricted method (8068ms)
1) Should successfully call the restricted method
2 passing (17s)
1 failing
1) Restricted Contract
Calling Functions
Should successfully call the restricted method:
AssertionError: expected promise to be rejected but it was fulfilled with { Object (result, decode, ...) }
3. Access restrict the smart contract
In order to restrict a method to be called only by the deployer of the smart contract we would need to modify the smart contract to know who is their deployer. Below is how to do that.
3.1 Modify the smart contract
Let’s first add state variable for the owner and set it on deploy time. On the second line of Restricted.aes put the following:
record state =
{ owner : address }
stateful entrypoint init() =
{ owner = Call.caller } // Initializing the owner to the deployer
This snippet initializes the state variable owner to the deployer of the contract.
With this done let’s create a function that checks the caller of an arbitrary transaction and throws if it is not the owner:
// Method to throw an exception if the expression exp is false
function requirement(exp : bool, err : string) =
if(!exp)
abort(err)
entrypoint onlyOwner() : bool =
// Require that the caller of this method
// is actually the deployer
requirement(state.owner == Call.caller, "The caller is different than the owner")
true
The onlyOwner function checks who has called the current transaction and reverts if it is not the deployer. Let’s use it inside our restricted function. Modify the restrictedFunction
method like this:
// Access restricted method. Should be callable only by the owner/deployer
entrypoint restrictedFunction(i: int) : int =
onlyOwner()
i
This will ensure that the deployer is the only one that can call this method. Let’s prove this through the unit tests.
3. Run the unit tests
Let’s run the unit tests again:
aeproject test
Output of aeproject test
:
===== Starting Tests =====
Restricted Contract
===== Contract: Restricted.aes has been deployed at ct_GvaesWjbs6bYpNPVNphX3SpzBfdp7JeDqA4Jtwq1hB9cnFgsY =====
✓ Deploying Restricted Contract (8893ms)
Calling Functions
===== Contract: Restricted.aes has been deployed at ct_2gJev7yya8cJvzaRAYKFGn2EooQyxDPpD6oUBXz9FiWzCUSYkz =====
✓ Should successfully call the non restricted method (7912ms)
✓ Should successfully call the restricted method (193ms)
3 passing (17s)
There is no sophia test to execute.
[]
All three tests pass successfully now!
When to use it – Philosophical dilemma
Although this is a very cool practice, the main reason for blockchain technology is to allow for decentralization of systems. Having an access being restricted to a certain user is somewhat similar to having a central point, although it is very well known. Use this technique with caution and think about possible implications of this elevated access.
Full code
contracts/Restricted.aes
:
contract Restricted =
record state =
{ owner : address }
stateful entrypoint init() =
{ owner = Call.caller } // Initializing the owner to the deployer
// Method to throw an exception if the expression exp is false
function requirement(exp : bool, err : string) =
if(!exp)
abort(err)
entrypoint onlyOwner() : bool =
// Require that the caller of this method
// is actually the deployer
requirement(state.owner == Call.caller, "The caller is different than the owner")
true
// Non access restricted method. Should be callable by any user successfully
entrypoint nonRestrictedFunction(i : int) : int =
i
// Access restricted method. Should be callable only by the owner/deployer
entrypoint restrictedFunction(i: int) : int =
onlyOwner()
i
test/exampleTest.js
:
const Deployer = require('aeproject-lib').Deployer;
const RESTRICTED_CONTRACT_PATH = "./contracts/Restricted.aes";
describe('Restricted Contract', () => {
let owner;
let ownerKeyPair = wallets[0];
let nonOwnerKeyPair = wallets[1];
let ownerDeployedContract;
before(async () => {
// Create client object
owner = new Deployer('local', ownerKeyPair.secretKey);
})
it('Deploying Restricted Contract', async () => {
// Deploy it
ownerDeployedContract = owner.deploy(RESTRICTED_CONTRACT_PATH);
// See if it has deployed correctly
await assert.isFulfilled(ownerDeployedContract, 'Could not deploy the Restricted Smart Contract');
})
describe('Calling Functions', () => {
it('Should successfully call the non restricted method', async () => {
ownerDeployedContract = await owner.deploy(RESTRICTED_CONTRACT_PATH);
// Call the non restricted method
const callNotRestrictedMethod = ownerDeployedContract.nonRestrictedFunction(3);
await assert.isFulfilled(callNotRestrictedMethod, 'Calling the non restricted function failed');
const callResult = await callNotRestrictedMethod;
await assert.equal(callResult.decodedResult, 3, 'The returned data was not correct')
})
it('Should successfully call the restricted method', async () => {
const nonOwnerCalling = await ownerDeployedContract.from(nonOwnerKeyPair.secretKey);
// Call the restricted method from non owner
const callRestrictedMethod = nonOwnerCalling.restrictedFunction(2);
// It should be rejected as it is restricted to owner only
await assert.isRejected(callRestrictedMethod);
})
})
})
Conclusion
It is pretty easy to add access-restriction to your contract methods. In a few simple steps you can have administrative layer functionality. What are some use-cases for you to use it? Feel free to get in touch with us with your ideas!
The æternity team will keep this tutorial updated with news. If you encounter any problems please contact us through the æternity dev Forum category.