Modularized code in Solidity

Since I’ve beed writing quite some Solidity code recently, this post we be sharing some tricks to write modularized code in solidity.

Make Library to act like an object

However, the trick here would be how to make a library to act like an object. In short, this is a combination of “struct” and the usage of “using … for”. Following is a toy example of a Human library that acts like an object.

library Human {
struct Object {
string name;
uint256 age;
}

function greeting(Object memory self) internal pure returns (string memory) {
return string(abi.encodePacked("Hi, I am ", self.name, "!"));
}

function publicAge(Object memory self) internal pure returns (uint256) {
if (self.age > 25) {
return 25;
}
return self.age;
}
}
contract Example {
using Human for Human.Object;

Human.Object alice;
Human.Object bob;

constructor() public {
alice = Human.Object("Alice", 30);
bob = Human.Object("Bob", 20);
}

function aliceGreeting() public view returns (string memory) {
return alice.greeting();
}

function aliceAge() public view returns (uint256) {
return alice.publicAge();
}

function bobAge() public view returns (uint256) {
return bob.publicAge();
}
}

Basically, in your library you define the data of “object” in a struct, and then you always use that struct as your first argument of your function. With the key word “using … for…”, you can magically pass in the struct data as first argument without explicit stating it.

The above example only uses “Object memory self” as first input arg. However, if you want to change the state, you can use “Object storage self” and do side effect on the struct data itself. Then this would update the storage data directly.

Why do we prefer this pattern over just using “contract” directly though? Though “contract” would act more like a real object/instance, but it would need to pay the cost of deploying the contract. If you just need some data for logic abstraction and not storage abstraction, it is obviously way too expensive to do so. Also, you can use control how you’d like to use the library function, as “internal” or “public”. Using “internal” can save you some gas cost while adding deployment cost. Sometimes your code is too large to deploy then you’ll need to make it “public” to save deployment gas.

However, there is limit and downside on this. The biggest one would be there is no “interface” for a library. Due to the fact that Solidity is a typed language, you cannot write code like script language and just rely on duck typing without defining an interface. As a result, you cannot have one interface with multiple implementation using this. This make the library to be coupled with the contract using it.

Registry contract with an interface

So you would have a registry contract to act like a factory or provider in OOP concept. And then you would deploy your implementation contract of an interface, register it to the registry, and whenever the code need to get the implementation, it would call the registry to get it.

interface Animal {
function move() external pure returns (string memory);
}
contract Bird is Animal {
function move() public pure returns (string memory) {
return "I fly";
}
}
contract Fish is Animal {
function move() public pure returns (string memory) {
return "I swim";
}
}
contract AnimalRegistry {
mapping (string => Animal) _animals;

function animals(string memory name) public view returns (Animal) {
return _animals[name];
}

function register(string memory name, Animal animalContract) public {
require(address(animalContract) != address(0), "Do not register empty contract.");
require(address(_animals[name]) == address(0), "Such animal has already beed registered");

_animals[name] = animalContract;
}
}
contract Example {
string constant BIRD = "bird";
string constant FISH = "fish";

AnimalRegistry registry;
constructor(AnimalRegistry _registry) public {
registry = _registry;
registry.register(BIRD, new Bird());
registry.register(FISH, new Fish());
}

function testBird() public view returns (string memory) {
return registry.animals(BIRD).move();
}

function testFish() public view returns (string memory) {
return registry.animals(FISH).move();
}
}

Though not shown in the example above, but one biggest reason of using registry pattern is that quite often we need to whitelist trustable contracts. As a result, you can use the registered contract as a trusted contract lists. In modularized contracts, there would be chances a contract calling another contract. You definitely want to make sure only things in your design is calling it, and not from an arbitrary contract from nowhere.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store