Flash loan for NFTs: How to instantly borrow NFTs
What if you can temporarily own the $2.9 million tweet NFT? What would you do?
A Little Background
Fundamentally, the Ethereum blockchain is a state machine. Each block stores a set of updates to the state of the world. In it, who just received which tokens and who just minted which NFTs are permanently etched into the blockchain’s history.
Smart Contracts determine the contents of these blocks. Contracts can move, create, and destroy assets like tokens and NFTs. At the end of its execution, the resulting changes are what get written down.
Suppose I execute my contract that sends one ether to you. Then in the next block, you will have one ether credited to your wallet. Obviously, an ether will be deducted from my wallet.
Then suppose if I execute a contract that sends one ether to you and immediately sends one ether back to myself all in one transaction? In this case, the blockchain will not record any movements in ether. And I will still have my ethers sans some gas fee.
This idea is significant because you can move around any accessible assets as you please as long as you return the assets back to their rightful owners by the end of the transaction. Any positive side effects produced within the transaction are fair game for you to claim.
Let’s say that DAI/Ether is swappable at different prices on a pair of exchanges. Let’s say exchange #2 is paying more for ethers per DAI. The contract can send one ether to exchange #1 and swap it to DAI. Then, it can send the DAIs to exchange #2 and swap it back to ethers. At the end of the transaction, the contract sends the ethers back to me. I will get back my one ether in principle plus some arbitrage profits.
Say Hello to Flash Loans
Here’s where it gets crazy. Many platforms will simply loan out millions of dollars worth of crypto for zero collateral as long as you can return the principal plus some fee. And while the token is temporary in your possession, you can do whatever you want with them.
Welcome to the world of flash loans. Most use cases of flash loans have revolved around ERC-20 tokens, and the original proposal, EIP-3156, too is focused on cryptocurrency use cases.
This article will try to extend flash loans to a different asset class, NFTs.
Flash Loaning the ERC-721
There are two primary interfaces for NFTs: ERC-721 and ERC-1155. I will focus on the ERC-721 spec as it is the most popular implementation.
Setting up the Stage
Let’s say there is a popular NFT called NotSoBoredMonkey. NotSoBoredMonkey will host an event next week, and all NFT owners can claim their tickets through the NotSoBoredMonkey’s smart contract. But unfortunately, our NFT holder, the lender, cannot attend the event.
Not wanting it to go to waste, given that our lender knows that thousands are on standby for the privilege to attend, he wants to find some way to have his cake and eat it too. Unfortunately, transferring the NFT to another user’s wallet is out of the question. While this technically works, there’s a fundamental flaw. Once the NFT leaves the lender’s wallet, the lender loses ownership of the NFT. A borrower of the NFT can choose not to return it after claiming the ticket, and there’s nothing the lender can do to get his NFT back.
Using flash loans instead, the lender can loan out his NFT risk-free, and the borrower can get her ticket.
The NFT Setup
NotSoBoredMonkey implements the ERC-721 spec with the method below, allowing the NFT owners to redeem their tickets. It checks whether the transaction sender is the rightful owner of the NFT and if so, it will assign the tickets to this wallet address.
// NotSoBoredMonkey.sol
function redeemTicket(uint256 id) external {
require(ownerOf(id) == tx.origin, "Tx origin is not the owner of this token");
require(_tickets[id] == address(0), "Ticket has been claimed");
_tickets[id] = tx.origin;
}
Understanding the Flash Loan Flow
There are two big ideas to understand with flash loans.
A smart contract can call another smart contract to execute arbitrary code.
Transactions in Ethereum are atomic. If there are any errors, all changes are immediately reverted.
Smart contracts can call other smart contracts, and it works like a remote procedure call. So while the code in a smart contract is static and unchangeable, contracts can call out to different contracts to perform various actions. In this way, one contract can be used as a proxy to perform many actions.
Transactions in Ethereum are atomic. This is synonymous with databases transactions. All changes commit as a whole, or all operations are reverted. In this sense, we can write a contract that sends the token to another party, calls a remote operation, and checks if the tokens are rightfully returned. If the tokens are not, the transaction will fail, which will nullify all changes. As a result, the token lenders can sleep well at night knowing that there is no way for the lender to lose their tokens.
// FlashLoanNFT.sol
function flashLoan(
address facilitatorAddress,
address erc721Contract,
uint256 tokenId,
bytes calldata params
) external payable {
IERC721 _contract = IERC721(erc721Contract);
address _lender = _contract.ownerOf(tokenId);
// confirm the lender has given this contract permission to move the NFT
require(
_contract.isApprovedForAll(_lender, address(this)),
"Lender has not approved contract's setApprovedForAll"
);
// loan the NFT to the borrower
_contract.safeTransferFrom(_lender, msg.sender, tokenId);
// perform flash loan actions the borrower specifies
IERC721FlashLoanFacilitator facilitator = IERC721FlashLoanFacilitator(facilitatorAddress);
require(
facilitator.executeOperation(
erc721Contract,
tokenId,
_lender,
params
),
"Execution failed"
);
// confirm that the NFT was returned to the lender
require(_contract.ownerOf(tokenId) == _lender, "NFT was not returned to the lender");
}
In the code above, the contract moves the NFT from the lender’s wallet to the borrower’s. Then it calls the smart contract code the borrower provided. Then checks that the NFT is returned to the lender. If any step fails, the whole transaction is reverted.
The Remote Contract Call
The borrower wants to temporarily own the NotSoBoredMonkey NFT so she can call the redeemTicket
function to redeem her ticket. The code below accomplishes this and concludes by returning the NFT to the lender.
// FlashLoanFacilitator.sol
function executeOperation(
address _tokenContract,
uint256 _tokenId,
address _lender,
bytes calldata
) external returns (bool) {
NFTWithRedeemable redeemable = NFTWithRedeemable(_tokenContract);
// use the loaned NFT
redeemable.redeemTicket(_tokenId);
// return the NFT back to the owner
redeemable.safeTransferFrom(tx.origin, _lender, _tokenId);
return true;
}
Conclusion
With these two short code snippets, anyone can loan out their NFT risk-free. Furthermore, this concept can be easily extended to include premiums. For example, a borrower must also attach a payment of 0.1 ether to borrow an NFT from a lender.
I have this implemented on my Github. Please go check it out for the entire working implementation.
Critiques
This design has two key limitations.
First, the lender must trust the smart contract enough to give it full access to the lender’s NFTs. Technically, the lender needs to setApprovalForAll
flag to true for the FlashLoanNFT.sol contract on a given NFT collection. The danger is that this flag gives a smart contract power to transfer away any NFT collection in the present and future. Malicious smart contracts can and have abused this feature in the past. Only perform this action on smart contracts you trust!
Second, the context of the facilitator contract is limited. Smart contracts track two addresses when executed: tx.origin
and msg.sender
. tx.origin
is the transaction initiator’s address, and msg.sender
is the wallet or contract address that called the current contract. So, when one smart contract calls another smart contract, the msg.sender
is the prior contract’s address. This design makes it impossible for a contract to impersonate a wallet perfectly. Hence this method will not work when a contract requires a direct transaction from the owner’s wallet.1
It is possible to use delegatecall
to force this, but this makes the FlashLoanNFT.sol contract itself vulnerable to attack.