Practical crypto: the Power of Building
The more I explore the blockchain world, the more I think that all this financial speculation hype around cryptocurrencies is obfuscating its real value and potential. Even when you hear someone talking about cryptos, the statements are always about what they invested in, not what they built with it.
To me, what is really exciting about web3 is actually the power to autonomously create and deploy things that were just unimaginable before without multiple 3rd parties.
So I asked myself, how would I show the other (non-financial) side of the medal to someone non-technical? And the answer was: let’s build something practical, something that already has a more classic counterparty, so I did!
But before we dive into our proof of concepts, it’s worth clarifying something about cryptos. Not every blockchain is suitable to build and run programs on it. This concept was introduced by Ethereum (the very first “Turing complete” blockchain), and later replicated, with various nuances, by other networks.
In this post, we will use Ethereum and Solidity (the most popular smart contract programming language) to build:
- Libretto: a deposit account for your kids
- Comitiva: a shared wallet for your group of friends
- Lotteria: a money-faucet to handle a sudden big prize (ok, unlikely but fun)
Ready? Let's go!
If you don't know what a smart contract is, think about it like a computer program that runs on the blockchain, stores data on-chain, and cannot be changed (unless its state and some variables allow it).
The first smart contract I wanted to showcase is "Libretto." It's an Italian word used to indicate a deposit account that people usually open for young kids, to store money and save it for university, a big gift when they turn eighteen, the car, or whatever. There are many types of accounts, but the most popular work like this:
- Anyone can deposit at any time
- Only the beneficiary can withdraw funds
- Sometimes, the withdrawal is also locked in time
Goes without saying that you need a bank or a financial institution to set up something like that (and it comes at a price), but not with crypto! You can use Ethereum to build the very same type of accounts, deploy them on the blockchain, and keep the same logic.
When you deploy it, the owner is automatically assigned to you (well, the address used to deploy it), and the lock time can be defined upon creation.
I also made a "giftable" version, where the beneficiary can be a different address from the deployer. Here is the code, pretty much straightforward, and here’s a GitHub repository with the two contract versions: https://github.com/francescocarlucci/libretto/
contract Libretto {
address payable public immutable owner;
uint256 public immutable unlockTime;
constructor(uint256 _unlockInYears) {
owner = payable(msg.sender);
unlockTime = block.timestamp + (_unlockInYears * 365 days);
}
modifier onlyOwner() {
require(msg.sender == owner, "Abort: caller is not the owner");
_;
}
modifier onlyIfUnlocked() {
require(block.timestamp > unlockTime, "Abort: funds are still locked");
_;
}
event Deposit(address indexed from, uint256 amount);
event Withdraw(address indexed to, uint256 amount);
function withdraw() external onlyOwner onlyIfUnlocked {
uint256 balance = address(this).balance;
(bool ok, ) = owner.call{value: balance}("");
require(ok, "Failed: ETH transfer failed");
emit Withdraw(owner, balance);
}
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
}
At this point, the question arises: who guarantees that anyone can deposit, but only the owner can withdraw, and only after the locking time? The answer is simple: the rules are encoded in the smart contract itself, immutable by definition.
modifier onlyOwner() {
require(msg.sender == owner, "Abort: caller is not the owner");
_;
}
modifier onlyIfUnlocked() {
require(block.timestamp > unlockTime, "Abort: funds are still locked");
_;
}
Those two modifiers ensure these rules, and the owner is written directly in the bytecode—it’s immutable. Of course, it is possible to make the owner stored and "updateable," but in this simple version I wanted to keep it brutally simple, as a proof of concept. But remember, we are on a blockchain... your "Libretto" balance will always be public! And here is mine!
At this point, pause and realize: in 20 lines of code, we created a secure deposit account—without a bank, without waiting times, without costs (apart from the gas to deploy it), and immediately active. In my opinion, this is the real revolution, and it’s still very much overlooked.
Comitiva: a shared wallet for your group of friends
Another tool I always wanted to have and share with my friends is an emergency shared wallet/account. Many of my friends have been long-time friends from school; some of them are more successful and financially stable, others less so. I always thought it would be cool to have a shared account where everyone can fall back if in need—and that is also possible with crypto!
So, here is Comitiva (which translates as group of friends in Italian), a very basic multi-signature wallet where anyone can deposit, but withdrawals require multiple approvals to be met. Like a configurable threshold: if you are sharing the wallet with five friends, you can decide that the threshold to make a transaction is two or three. This means that two or three people from the group have to approve an expense/withdrawal—which can be submitted by anyone!
The contract is not gas-optimized, but it is suitable for a small group of people. It misses some basic features, like adding people to the owners group or updating the threshold, but it works pretty well as a basic PoC. Maybe you want to add those features? Go ahead and fork it!
/// @notice All owners are assumed to be externally owned accounts (EOAs), not smart contracts.
/// @dev Since owners are EOAs, reentrancy protection is not required for executing transactions.
contract Comitiva {
address[] public owners;
uint public threshold;
struct Transaction {
address to;
uint value;
bool executed;
uint confirmations;
}
Transaction[] public transactions;
mapping(uint => mapping(address => bool)) public isConfirmed;
constructor(address[] memory _owners, uint _threshold) {
require(_owners.length > 0);
require(_threshold > 0 && _threshold <= _owners.length);
owners = _owners;
threshold = _threshold;
}
modifier onlyOwner() {
bool ownerFound = false;
for (uint i = 0; i < owners.length; i++) {
if(owners[i] == msg.sender) {
ownerFound = true;
break;
}
}
require(ownerFound, "Abort: not an owner");
_;
}
modifier txExists(uint _txId) {
require(_txId < transactions.length, "Transaction does not exist"); // _txId within the bound of the array
_;
}
modifier notExecuted(uint _txId) {
require(!transactions[_txId].executed, "Transaction already executed");
_;
}
modifier notConfirmed(uint _txId) {
require(!isConfirmed[_txId][msg.sender], "Transaction already confirmed");
_;
}
receive() external payable {}
function submitTransaction(address _to, uint _value) external onlyOwner {
transactions.push(Transaction({
to: _to,
value: _value,
executed: false,
confirmations: 0
}));
}
function confirmTransaction(uint _txId) external onlyOwner txExists(_txId) notExecuted(_txId) notConfirmed(_txId) {
isConfirmed[_txId][msg.sender] = true;
transactions[_txId].confirmations += 1;
if (transactions[_txId].confirmations >= threshold) {
executeTransaction(_txId);
}
}
function executeTransaction(uint _txId) internal txExists(_txId) notExecuted(_txId) {
Transaction storage pendingTransaction = transactions[_txId];
require(pendingTransaction.confirmations >= threshold, "Not enough confirmations");
pendingTransaction.executed = true;
(bool success, ) = pendingTransaction.to.call{value: pendingTransaction.value}("");
require(success, "Transaction failed");
}
function getOwners() external view returns (address[] memory) {
return owners;
}
}
Would you share this wallet with your friends? :)
Lotteria: just in case you won the lottery
I know, this doesn't look like a realistic scenario to many, but while I was drafting this post, I heard on the radio yet another story of a big lottery win that ended in wasted money and mismanagement!
So, I thought it would be nice to show how you can use a simple smart contract to store your winnings and dilute the payments over time with a defined withdrawal limit.
contract Lotteria {
address payable public immutable owner;
uint256 public immutable maxWithdrawal;
uint256 public lastWithdrawal;
constructor(uint256 _maxWithdrawal) {
owner = payable(msg.sender);
maxWithdrawal = _maxWithdrawal;
lastWithdrawal = 0;
}
modifier onlyOwner() {
require(msg.sender == owner, "Abort: caller is not the owner");
_;
}
modifier oncePerMonth() {
require(block.timestamp >= lastWithdrawal + 30 days, "Abort: only once per month is allowed");
_;
}
event Withdraw(address indexed to, uint256 amount);
function withdraw(uint256 _amount) external onlyOwner oncePerMonth {
require(_amount <= address(this).balance, "Abort: insufficient balance");
require(_amount <= maxWithdrawal, "Abort: maximum amount exceeded");
lastWithdrawal = block.timestamp; // this is a very basic Reentrancy protection, but good for the purpose
(bool ok, ) = owner.call{value: _amount}("");
require(ok, "Failed: ETH transfer failed");
emit Withdraw(owner, _amount);
}
receive() external payable {}
}
I understand many won't trust a smart contract to hold millions on your behalf, but... would you trust banks on crypto custody services? Oh, I just found another interesting blog post idea!
Not only finance
I know, reading this blog it might look like we can only build financial applications on the blockchain, but that is absolutely untrue. I just wanted to give tangible ideas that are close to everyone, and I think everyone has a deposit account or a wallet.
On the blockchain, we can also build games, ticketing systems, ID verification, voting platforms, tracking solutions, and much more. And remember—you don't need a server or a domain to deploy. You deploy on the blockchain using just your wallet, get back an address, and you're live... anyone can now interact with your "app."
To me, this may very well be the future!