My deep dive into counterfactual instantiation started with pondering this tweet and formulating possible solutions to the problem posed:
Currently, repayment of a Dharma loan requires a couple steps for the recipient. First, they have to approve a contract to transfer their tokens. Then, they have to call a function on the contract to initiate the payment. For smart contract nerds this is trivial but the UX is a bit clunky for a novice in the space. I’m always looking for ways we can do a little extra work upfront to make the user experience smoother.
What Nadav Hollander is looking for here is a way to give the recipient of a loan an Ethereum address for repayment that they can simply transfer tokens to. The problem is this is really hard to do without a trusted third party because once the tokens are moved to the address they still need to approve the repayment contract. Then, trigger the function in the contract for everything to be transferred and registered on-chain.
After rapping with Philippe Castonguay on Twitter for a while we came up with what I think is a pretty elegant solution. The idea is to craft a transaction that would eventually deploy a contract to approve the transfer of the funds and call the repayment function. We wouldn’t deploy the contract right away, but using a trick we can predict the eventual address of the contract. This address is where the recipient of the loan could send their repayment.
This trick called ‘Counterfactual Instantiation’ was introduced to me by Alex Van de Sande and George Spasov in this medium article. The idea has been around for a while before… check out all the references in this article. Plus, this article from Nick Johnson about hacking up transaction signatures. Heck, Liam Horne & Jeff Colman even have a state channel protocol named Counterfactual. (Here is their most recent development)
I think of “Counterfactual Instantiation” as predictive deployment: You know exactly what code will be deployed using keyless signatures to a predetermined contract address. The transaction is crafted ahead of time, only deployed if needed, and can’t be altered.
The reasoning for the added complexity on the Dharma side is the user only needs to transfer tokens, not call any functions. This means any wallet that supports the transfer of ERC20 tokens can facilitate the repayment instead of a custom dApp.
Predictive Deployment Toolkit
As you can see, in line 11 we craft a transaction to deploy our contract. Then we sign that transaction with a dummy account. Then we carve out the last 134 characters of the transaction and replace them with an arbitrary random number and a specific constant. (I am not smart enough to explain why, but this is link is a good start. From what I can decipher it is a constant picked for Secp256k1 that “significantly reduces the possibility that the curve’s creator inserted any sort of backdoor”? I’ve also added four cows for the same reason: #beefbeefbeefbeef 😅 🐮🐮🐮🐮!)
Finally, once we have this single-use transaction signed, (with a private key we never knew) it is a valid Ethereum transaction and we can determine what address it will have when it goes on-chain. (If it goes on-chain! In the case of counterfactual instantiation for dispute resolution, most contracts never make it to the blockchain... Just having the contract and proving it will do something deterministic on-chain is usually enough to keep everyone honest.)
You can use this craft.js script with the following command:
node craft.js ABI_FILE BYTECODE_FILE GAS_LIMIT GAS_PRICE [...CONSTRUCTOR ARGUMENTS]
Given some contract that you have already compiled, you pass in a path to the ABI and the bytecode along with gas constraints and any arguments that will be sent to the contract’s constructor on deployment. This script will then craft a single-use transaction that will eventually deploy the contract. It is also going to predict what address the contract will have once deployed:
Notice that this also lets you know the from address. This is the account that will have to pay the gas to deploy the contract once the transaction is sent to the network. Therefore, before we run the transaction we will need to fund the from address.
I hope to eventually build on this script and create a predictive deployment toolset to make it easier for other developers. Hit me up if you are interested in something like this!
Another fun use case for predictive deployments is Blockie (Identicon) mining. You can run through thousands of different possible contract addresses until you find one with a specific Blockie or maybe even leading zeros for factory contracts? Read more about blockie mining here.
Predictive Deployment Loan Repayment
Now that we have this tool, what can we build with it? Well let’s go back to Nadav’s problem. Wouldn’t it be nice if Dharma could loan someone a grip of tokens and give them a simple address for repayment? Could this work without a trusted third party? Could we use a counterfactual contract to eventually sweep the funds and call the correct on-chain functions? 🤔🤔🤔
I’ve written a small proof-of-concept and I’d love to dig into the code but do it in a way where anyone else could also contribute and extend the work. Let’s clone down the shared repo and take it step-by-step:
Since some of my dependencies for Clevis are sometimes hard to install I have a handy Docker container that will bring up an environment along with a local blockchain like Ganache and the React frontend:
docker run -ti --rm --name clevis -p 3000:3000 -p 8545:8545 -p 1337:1337 -v ~/counterfactual-token-repayment:/dapp austingriffith/clevis:latest
Notice that we point the Docker container at the repo we cloned. This is so we can share code between the container and our native filesystem. To edit the code we can simply open that directory in a new terminal with our editor of choice like Atom:
Your Docker container will take quite some time to get the environment all prepared, but eventually you will get a prompt that looks like this:
This container also brought up our local blockchain and our frontend so we can visit it here:
You will probably end up getting an error like this. We are working on making this look better in Dapparatus, but it means your contracts aren’t compiled and injected into the frontend yet. To do that, let’s run a full compile, deploy, test, and publish:
🗜️ Clevis:/dapp 🗜️ clevis test full
Note: the “🗜️ Clevis:/dapp 🗜️” part is the prompt, don’t actually run that. The command you want to run within the Clevis container is “clevis test full”.
When that finishes, the frontend should come up:
To replicate a loan system, I created an oversimplified Loan smart contract that keeps track of loans on-chain with an issue() and a repay() function. An account with the correct permission can issue() loans and anyone can approve (ERC20) tokens and call repay() to repay the loan.
Next, I created a simple Sweeper smart contract that will land at a predicted address, approve the transfer tokens that are already at the address, call the repay() function on the Loan contract, and destroy itself:
Finally, I’m also deploying an example ERC20 token called SomeCoin to represent any token that follows the ERC20 standard.
If you are a developer following along, you’ll want to edit the metamask() test suite in tests/clevis.js to add in your address so you will receive a little ETH each time the contracts are deployed:
To receive your ETH you can do a full redeploy with:
clevis test full
If you inspect line 154 we are also minting 5000 SomeCoins to the Loan contract to provide us with tokens to loan to users. The frontend shows us the address of the Loan contract and the current balance of SomeCoins:
Awesome. Before we can actually issue loans, we will need to prepare our predictive deployment scripts. As we saw above, the counterfactual crafting script takes in an ABI and bytecode so let’s compile our Sweeper.sol to bytecode:
🗜️ Clevis:/dapp 🗜️ clevis compile Sweeper
Now we can launch our backend script that will handle everything for us with simple ajax calls from our frontend:
🗜️ Clevis:/dapp 🗜️ node backend.js
Now we are ready to issue a loan to our metamask user from the frontend:
When we hit issue a lot of things happen and we should see a new loan and the token balance of the metamask user should adjust to 100:
Notice that we also have a simple address to return our tokens to when we are done using them! Rad. Okay. Whoa. What all just happened?
First, the React app POSTed the loanAmount and loanRecipient to the backend server:
As you can see here in the backend.js it crafts the counterfactual transaction using the script demoed above by passing in the Sweeper:
The result of this crafting is returned to the frontend and that’s how it displays the address we need to return our tokens too. But, we can also query the data from the backend using the /tx/ endpoint and our loan id:
This is the output from our counterfactual/craft.js script we explored above. If the transaction is posted to the Ethereum network, our Sweeper contract will be deployed from account 0x69D3Ec… and its address will be 0x4D8e539…
But! We don’t want to deploy that contract yet. Maybe the loan will be repaid in the traditional way or maybe it won’t ever be repaid at all. Either way, we will have to wait and see. An off-chain script can run regularly and watch the state of our loan:
Let’s go ahead and transfer the 100 SomeCoins to the repayment address:
And when the tokens arrive at the address, our frontend reflects the change:
The counterfactual address where the Sweeper will be deployed to now contains the correct amount of tokens and we are ready to deploy. Let’s hit the Collect button and fire off the repayment sweep:
Neato gang! The Loan contract now contains the original 5000 SomeCoins and the loan is marked as PAID on-chain. Let’s inspect what just happened:
The frontend tells the backend to collect the loan of a specific id. The backend first sends a little ETH to the keyless from address so it has enough funds to deploy the contract. Then, the backend runs the counterfactual/deploy.js which sends our transaction to the Ethereum network to deploy, approve, sweep, and destroy.
To further unpack all of this, we deployed the Sweeper.sol contract to the predicted location that was holding the 100 SomeCoins. The Sweeper then approved the Loan contract to move the tokens and called the repay() function:
Then the repay function marked the loan as repaid and collected the tokens:
Finally, the Sweeper destroyed itself to clean up on-chain storage.
There are few things to mention that I’d love to get some feedback on from the community. First, the counterfactual transaction is very sensitive. The gas constraints and nonce are all signed and you can’t adjust them. If the transaction runs before there are enough tokens or the gasLimit is wrong, there is a chance that the transaction will fail and the tokens will be locked up forever in an account we will never know the private key of. 😪😪😪
A smaller but worth mentioning issue is the deploying from address will be left with some ETH that we will never get back. It is isn’t much, but that adds some overhead costs.
I bet some smartypants out there can figure out some possible solutions and how we can collect those leftover funds from the contract destroy?
Using this method, we were able to simplify the process for the end user by adding a little extra complexity on our side. Instead of the user having to approve() the Loan contract and call the repay() function themselves. They were able to just transfer() the tokens to a repayment address and our off-chain cron will automatically sweep the tokens. This is a much easier task for a new user in the space. If we are going to drive mass adoption in Ethereum, we need to make the UX as smooth as possible. But maybe it isn’t worth it? Maybe just a good UX around the approve() and repay() is good enough?
Thanks for kicking through this exploration with me. If you’d like to get in touch, please do! I’m @austingriffith on telegram/twitter and I try to keep some of my projects and articles posted up at: https://austingriffith.com Huge thanks to my dude Kevin Owocki for letting me rep Gitcoin Labs.
To learn more about Gitcoin, click below. We welcome you on our journey to grow open source while changing the way we work.