Skip to content

A simple implementation of a multi-signature wallet built in Solidity and cooked in some 🌢️ foundry sauce.

Notifications You must be signed in to change notification settings

czar0/multisig-wallet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

9 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

MultiSig Wallet ✍🏼✍🏽✍🏿

A simple implementation of a multi-signature wallet built in Solidity and cooked in some 🌢️ foundry sauce.

Think of a multi-signature wallet as a supercharged safe or a high-tech joint bank account. Instead of just one key, multiple keys are needed to open it. It's all about sharing the responsibility, like many friends each having a piece of a secret code. More keys mean more security. And hey, if my description made zero sense πŸ€·β€β™‚οΈ, check out this awesome article to get the full scoop.

What can you do with it? (for now)

  • 🫸 submitting a new transaction (to be reviewed and approved by the wallet owners)
  • πŸ‘ approving the submitted transaction (and waiting for other owners to approve)
  • πŸ™…β€β™€οΈ revoking an approval vote on a transaction (if you changed your mind)
  • βš™οΈ executing the transaction (once the approval quorum has been reached)

The main purpose of this repository is to provide some examples of what concerns:

  • Solidity best practices and style guide
  • Foundry test cases (faithful with UncleBob's approach - 3 laws of TDD - evergreen article)
  • Foundry script instructions
  • Foundry quick deploy&run
  • Foundry contract interactions

You will get to know the entire foundry suite and feel like you're the boss of your crypto! (I mean, imagine that? πŸ˜„)

Requirements

  • solc - solidity compiler
  • solc-select - manages installing and setting different solc compiler versions (recommended)
  • foundry (see below πŸ‘‡)

Make sure your solidity compiler matches with the minimum specific version or version range defined in the contracts.

KYF (Know your foundry πŸ› οΈ)

Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.

Foundry consists of:

  • Forge: Ethereum testing framework (like Truffle, Hardhat and DappTools).
  • Cast: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
  • Anvil: Local Ethereum node, akin to Ganache, Hardhat Network.
  • Chisel: Fast, utilitarian, and verbose solidity REPL.

Install foundry suite

Download and install foundryup:

curl -L https://foundry.paradigm.xyz | bash

Run it to install the full suite:

foundryup

If you are running into errors, just check out the book right here --> πŸ–οΈπŸ“•

Getting started

Setup

Setting up the environment variables:

cp .env.example .env
source .env

The default values contained in this file will work just fine, but you are free to make any change you like (e.g. modifying the addresses, changing the number of owners, etc.), keeping into account a few things: see comments in the .env.example file.

Building (aka compiling)

To build the contracts simply run:

forge build

By default, this will compile all the contracts contained in the lib, script, src, test directories and store the artifacts ABIs in the out folder.

If you prefer to build only the files contained in a specific folder (e.g. the src), you can simply run it with the -C <PATH> flag:

forge build -C src

Testing

Easy as:

forge test

This will compile and run all files within the test folder. To test a specific contract use the flag --match-path as follows:

forge test --match-path test/<CONTRACT>

You can set different levels of verbosity simply by adding thooooousands vs as trailing parameters, like that:

forge test -vvvv

Deploying

From now on, I would recommend opening a side tab in the terminal as we will need to execute a local client - anvil - which is the cool-runner and younger brother of ganache and hardhat-node's family.

So, let's run it in one tab:

anvil

and keep it open!

In the second tab (we will refer to this as the mess-things-up-tab) run:

forge script script/MultiSigWallet.s.sol:MultiSigWalletScript --rpc-url ${LOCAL_RPC_URL} --private-key ${PRIVATE_KEY} --broadcast

If our deployment is successful, we will see something similar to this:

βœ…  [Success]Hash: 0xb8a1fe732721d8896cbd12fad87c3657e62831ab9e86f570595732a57ebe7c40
Contract Address: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Block: 1
Paid: 0.003781237914220259 ETH (1000253 gas * 3.780281503 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.003781237914220259 ETH (1000253 gas * avg 3.780281503 gwei)

Grab the contract address and let's store it in an environment variable as we will need it later:

export CONTRACT="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"

Doing (actual) stuff

And now, the so-long-awaited fun πŸ˜„ - let's make some contract calls, shall we?

In the mess-things-up-tab, we will start with a simple query:

cast call ${CONTRACT} "owners(uint256)(address)" 0

If things go as planned, you will see the address of the first owner (if not...guess it is bug hunting time! πŸ›πŸ”)

GMing wealth πŸ’Έ

Alright, now that we have gotten our feet wet, let's dive deep and see this multisig in action!

The plot

A trio of crypto enthusiasts, united under a DAO, to gather people to invest in their project, have crafted a series of intricate Solidity puzzles. They presented these challenges on their Discord platform with a very generous offer: the first user to decode all the puzzles within 24 hours would be rewarded with 1 ETH. A week later, they proudly announced the lucky winner, and they are ready to transfer the bounty from their multisig account.

0 . πŸ’° fund

As OWNER1, funds the contract with 5 ether:

cast send --private-key ${OWNERS_PK[1]} ${CONTRACT} --value 5ether

I recommend prefixing the command body with the --private-key flag, so that we know right away who is executing what (like the subject/persona of a story).

Verify the contract is now funded:

cast balance ${CONTRACT}

1 . 🫸 submit

Submit (only propose) the transaction which will send:

  • 1 ether
  • to RECIPIENT
  • setting expiration time to 86400 seconds (24 hours)
    • Execute this command to assign this value to an environment variable: EXPIRATION=$(($(cast block latest -f timestamp) + 86400))
  • with message gm (hexed)
    • you can use cast from-utf8 <text> command
cast send --private-key ${OWNERS_PK[1]} ${CONTRACT} "submit(address,uint256,uint256,bytes)" ${RECIPIENT} 1ether ${EXPIRATION} 0x676d

Note that we are using cast send to sign and publish a transaction (this will alter the world state).

Let's check our transaction was correctly inserted:

cast call ${CONTRACT} "transactions(uint256)(address,uint256,uint256,bytes,bool)" 0

2 . πŸ‘ approve

As the default threshold policy is set to the uint(numberOfOwners/2 + 1) (ceiling), for 3 owners we will require uint(3/2 + 1) = 2 approvals (*spoiler: nevertheless, as any well-respected story, there will be a twist, get ready 🍿):

OWNER1 approves:

cast send --private-key ${OWNERS_PK[1]} ${CONTRACT} "approve(uint256)" 0

Let's verify:

cast call ${CONTRACT} "approved(uint256,address)(bool)" 0 ${OWNER1}

Let's move ahead approving the transaction also with OWNER2:

cast send --private-key ${OWNERS_PK[2]} ${CONTRACT} "approve(uint256)" 0

And let's verify it was approved:

cast call ${CONTRACT} "approved(uint256,address)(bool)" 0 ${OWNER2}

Alright, we hit our mark and were about to seal the deal, but...hold up! What's going on here? 😳

3 . πŸ™…β€β™€οΈ revoke

Out of nowhere, OWNER2 gets cold feet. Instead of moving forward, they pull the plug and take back their okay (seriously, not cool 😑):

cast send --private-key ${OWNERS_PK[2]} ${CONTRACT} "revoke(uint256)" 0

Quick check to see the damage:

cast call ${CONTRACT} "approved(uint256,address)(bool)" 0 ${OWNER2}

And yup, it is as bad as we thought. Our lucky 🐰 might not get their prize, and people might just lose faith in the DAO project: a total disaster 😭.

And, when we think everything is lost...a masked hero 🦸🏿 comes to the rescue...it's OWNER3! (what a plot twist! πŸ™„):

cast send --private-key ${OWNERS_PK[3]} ${CONTRACT} "approve(uint256)" 0
cast call ${CONTRACT} "approved(uint256,address)(bool)" 0 ${OWNER3}

4 . βš™οΈ execute

OWNER1 can finally execute the transaction.

But before jumping there, let's check first the initial balance of the recipient:

cast balance ${RECIPIENT}

and now let's execute:

cast send --private-key ${OWNERS_PK[1]} ${CONTRACT} "execute(uint256)" 0

And there it is:

cast call ${CONTRACT} "transactions(uint256)(address,uint256,uint256,bytes,bool)" 0

And our lucky 🐰 can count their money πŸ€‘:

cast balance ${RECIPIENT}

Happy ever after ✨

dadadadaaann --- THE END (closing credits...)

Other useful commands

Formatting

If you are unhappy with your "prettifier" or just tangled in indentation hell, look no further, foundry has a nice 🎁 for you:

forge fmt

Gas freak?

If you are working on gas optimization and want to check the before-after effect of your hopefully-well-rewarded work, try these out:

# slower but more comprehensive
forge test --gas-report
# faster and stored in a file
forge snapshot

More on gas tracking.

Making changes

The user is encouraged to mess-things-up (not just on a tab), break everything apart and make it work again - the best way of learning! (a wise 🐨 said).

Known issues

Continue learning

About

A simple implementation of a multi-signature wallet built in Solidity and cooked in some 🌢️ foundry sauce.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published