One of the beautiful things about smart contracts is they can be permissionless where the developers relinquish control to the community and all users are treated equally. However less than 20% of the top 50 protocols by TVL on Ethereum are using fully permissionless contracts. In this article we will look at why this is, the security considerations and how to deploy contracts from a parent contract so the owner logic is itself solidity code.
- Where DeFi Went Wayward
- Security Considerations
- Deploy A Contract From Solidity
- Video Demo Deploying A Contract
- Permissionless Smart Contracts
Where DeFi Went Wayward
If there is one product that has contributed to the move away from permissionless contracts it would be OpenZeppelin’s Ownable library. This has been around since 2016 and I use it all the time, in fact I love OpenZeppelin’s libraries. In certain circumstances where you want private code or special permissions are necessary it’s wonderfully simple. Just add ownerOnly as a modifier to the contract function and only the deployer can execute that function.
import "@openzeppelin/contracts/access/Ownable.sol";
contract PermissionedContract is Ownable {
function privateFunc() external onlyOwner {
// only contract owner/deployer can execute
}
}
Today it is common practice to have upgradeable contracts, pausable contracts, fund recovery functions and other patterns where the contract owner of other users have special permissions over everyone else.
There are some strong arguments as to why permissioned contracts are more secure which we will look at. However I feel that if we accept this as a rule then we might as well be using web2 databases rather than expensive, slow, difficult blockchains. As developers we take on the drawbacks and limitations of blockchain technology for the opportunity to write permissionless, immutable code on a decentralized peer to peer network.
Security Considerations
It’s worth mentioning that some of the top minds in the industry disagree with me on this. Consensys is famous for hiring the vast majority of competent Solidity developers (probably why I haven’t had an offer) and they have this to say:
The approach we advocate is to “prepare for failure”. It is impossible to know in advance whether your code is secure. However, you can architect your contracts in a way that allows them to fail gracefully, and with minimal damage. This section presents a variety of techniques that will help you prepare for failure.
- Pause the contract when things are going wrong (circuit breaker)
- Manage the amount of money at risk (rate limiting, maximum usage)
- Have an effective upgrade path for bug fixes and improvements
https://consensys.github.io/smart-contract-best-practices/
The general idea is that by having control over the smart contracts you can try to fix things when something goes wrong.
Perhaps more contracts have been rug pulled by owner privileges than have been saved by circuit breakers?
I have a strong opinion here and concede that in certain circumstances it makes sense to have access control functions. If a project is centralized by design or requires permissioned functionality for one reason or another. But if it’s possible to create a fully decentralized permissionless protocol then I believe that someone will eventually.
Deploy A Contract From Solidity
Next time you go to import the Ownable.sol library ask yourself if a parent contract could carry out the function of the owner. We can deploy child contracts from within Solidity like so:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.15;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MYTKN") {
_mint(msg.sender, 1e30);
}
}
contract Deployer {
address public myToken;
mapping (address => uint256) public claimed;
constructor() {
myToken = address(new MyToken());
}
function distribute() external {
require(claimed[msg.sender] == 0, "Already claimed");
claimed[msg.sender] = 100;
MyToken(myToken).transfer(msg.sender, 100);
}
}
Because MyToken is deployed via the deployer contract all the tokens are minted to that contract. We have a distribute function which gives each address 100 tokens. Obviously this is just an example as it is susceptible to sybil attacks but you could create more secure complex functionality such as DAO voting systems. The main idea here is that the token is owned by code rather than a developer.
When testing or from ethers.js we can call the following to get the token contract address:
const tokenAddress = await Deployer.myToken();
These contracts can never be paused or upgraded because no one has extended permissions. However removing the developer as a controller removes the potential for human error and social engineering attacks.
If you experiment with this you’ll soon come to a point where you have a chicken and egg problem where you need both contracts to know about each other. We can update a contract address from the constructor argument post launch as follows:
address public parent;
function setParentAddress(address _parentAddress) external {
require (parent == address(0x0), "Can only set once");
parent = _parentAddress;
}
From there you can interact between contracts to build out a permissionless system or at least reduce the dependency on access control.
Deploy From Solidity
Permissionless Smart Contracts
While there’s a reasonable chance I am being naive I believe that permissionless smart contracts running on decentralized peer to peer networks like Ethereum will change the world, or at least the world of finance. Perhaps it will be regulation that forces developers to move towards permissionless protocols where they can’t be targeted by the SEC.
In the long-term a survival of the fittest ecosystem of DeFi protocols will prove which are secure and anti-fragile. For developers I plead don’t use access control libraries unless you absolutely have to.