Introduction
In this tutorial, ill introduce the Permit2 contract from Uniswap Labs, a battle tested and audited contract which offers a unified approach to token approvals, enabling integrating Dapps to simplify User Experience for token approvals and also save on gas costs. Dapps on CELO can leverage this approach to enable an enhanced and secure token approval and better User Experience overall.
Prerequisites
This tutorial assumes a decent knowledge of solidity and smart contracts development, also the following tools would be needed to follow along:
Getting Started
At first glance, the essence of the permit2 library might not be obvious. Consider a senerio where a user say Alice, wants to interact with a DEX to perform a token swap, the following steps are in order:
- Alice first sends a transaction, approving the DEX some allowance to her token.
- Alice sends another transaction, which then transfers her previously approved tokens to the DEX, before the DEX swap logic can occur.
Now the problem with this approach for basic ERC20 token is that it involves sending two separate transactions and both transactions must be sent everytime Alice interacts with a different Dapp, which is not ideal from a user experience perspective.
However by integrating the Permit2 Library, the flow become much better through the following steps:
- Alice only have to approve her token for the permit2 contract. Ideally approve a max uint256 value for the permit2 contract. This transaction is only meant to happen just once.
- Alice can transfer her token to any Dapp which integrates permit2, by using her signature in one single transaction, and hence no previous approval transaction is required.
This lends well to a much better User Experience, and the permit2 library guarantees that a signature cannot be reused, by using an in-built nonce validation scheme.
Now for any Dapp that integrates Permit2, the initial approval transaction can be completely side stepped for the same token, which is pretty nice!
Contract Setup
We would setup the smart contracts using the Remix editor with the following steps:
- create a new workspace from remix, i have named mine permit2-integration
- create a new contract file, ‘permit2.sol’ and paste the code below:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
interface IPermit2 {
struct TokenPermissions {
// Token to transfer.
IERC20 token;
// Amount to transfer.
uint256 amount;
}
// The permit2 message.
struct PermitTransferFrom {
// Permitted token and amount.
TokenPermissions permitted;
// Unique identifier for this permit.
uint256 nonce;
// Expiration for this permit.
uint256 deadline;
}
// Transfer details for permitTransferFrom().
struct SignatureTransferDetails {
// Recipient of tokens.
address to;
// Amount to transfer.
uint256 requestedAmount;
}
// Consume a permit2 message and transfer tokens.
function permitTransferFrom(
PermitTransferFrom calldata permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes calldata signature
) external;
}
contract Permit2Clone is IPermit2 {
constructor() {
bytes memory bytecode = hex"6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1d...";
assembly {
return(add(bytecode2, 32), mload(bytecode2))
}
}
function DOMAIN_SEPARATOR() external view returns (bytes32) {}
function permitTransferFrom(
PermitTransferFrom calldata permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes calldata signature
) external {}
}
This bytecode has been shortened and can be obtained from the deployed address at 0x000000000022d473030f116ddee9f6b43ac78ba3 on etherscan at this location. I used the bytecode from etherscan, as the Permit2 deployment on CELO mainnet is not verified as at writing this article, but however its deployed to the same address.
Next we can proceed to create a simple ERC20 token contract ‘TestToken.sol’ and populate the file with the code below:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
import {ERC20} from "https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol";
contract TestToken is ERC20 {
constructor() ERC20("Test", "TT", 18){}
function mint(address owner, uint256 amount) external {
_mint(owner, amount);
}
}
Now we can create our Savings contract ‘savings.sol’ which integrates the Permit2 library
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
import {IPermit2} from "./permit2.sol";
import {IERC20} from "./permit2.sol";
contract Savings {
IPermit2 public permit2;
uint256 withdrawTimestamp;
mapping(address user => mapping(IERC20 token => uint256 amt)) public usersTokenBalance;
constructor(IPermit2 _permit2) {
permit2 = _permit2;
withdrawTimestamp = block.timestamp + 1 minutes;
}
function depositERC20Token(
IERC20 _token,
uint256 _amount,
uint256 _nonce,
uint256 _deadline,
bytes calldata _sig
) external {
usersTokenBalance[msg.sender][_token] += _amount;
permit2.permitTransferFrom(
IPermit2.PermitTransferFrom({
permitted: IPermit2.TokenPermissions({
token: _token,
amount: _amount
}),
nonce: _nonce,
deadline: _deadline
}),
IPermit2.SignatureTransferDetails({
to: address(this),
requestedAmount: _amount
}),
msg.sender,
_sig
);
}
function withdrawSavings(IERC20 _token, uint256 _amount) external {
require(block.timestamp >= withdrawTimestamp, "withdraw time not ellapsed");
usersTokenBalance[msg.sender][_token] -= _amount;
_token.transfer(msg.sender, _amount);
}
}
The savings contract allow an account to deposit any ERC20 token into the contract, by means of the depositERC20Token function, which can then be withdrawn after the withdrawTimestamp has ellapsed. We use a one minute timestamp from the current block timestamp for this demo.
This contract demonstrates how the permit2 library can be integrated which is surprisingly not difficult atleast for our usecase, we achieve this by:
- setting the permit2 address in the constructor
- calling the permitTransferFrom function in the depositERC20Token external function, and passing the required arguments for the ‘PermitTransferFrom’ struct, ‘SignatureTransferDetails’ struct, sender address and the signature signed by the owner of the token.
This function is the main entry point of the contract, and its arguments stated above are explained here.
Lastly we need one missing piece, a helper function to generate the EIP712 hash which would be signed by the token owner and then send as part of the call to the permit2 contract.
So we would create a new contract SignPermit.sol and populate with the following code:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "./permit2.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract SignPermit {
Permit2Clone permit2;
bytes32 constant TOKEN_PERMISSIONS_TYPEHASH =
keccak256("TokenPermissions(address token,uint256 amount)");
bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH =
keccak256("PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)");
constructor(Permit2Clone _permit2) {
permit2 = _permit2;
}
function getEIP712Hash(IPermit2.PermitTransferFrom memory permit, address spender)
external
view
returns (bytes32 hash)
{
return keccak256(abi.encodePacked(
"\x19\x01",
permit2.DOMAIN_SEPARATOR(),
keccak256(abi.encode(
PERMIT_TRANSFER_FROM_TYPEHASH,
keccak256(abi.encode(
TOKEN_PERMISSIONS_TYPEHASH,
permit.permitted.token,
permit.permitted.amount
)),
spender,
permit.nonce,
permit.deadline
))
));
}
function getTimestamp() external view returns(uint256) {
return block.timestamp + 10 minutes;
}
function timestamp() external view returns(uint256) {
return block.timestamp;
}
function domainSep() external view returns(bytes32) {
return permit2.DOMAIN_SEPARATOR();
}
}
This contract enshrines definition of some data need to output an ERC712 compatible hash. More on ERC712 here. The essence of the function getTimestamp is to get a time in the future for signing and using the permit2 signature, before expiry, (Please pardon my lack of descriptive name for this function).
With these in place, the directory in remix should look similar to the image displayed below:
Now we can proceed to compiling and deploying the contracts and then calling it from Remix UI, taking the following steps:
- First deploy the permit2 contract and note the deployed address
- Next deploy the TestToken contract
- Then deploy the savings contract, passing the permit2 address from above to its constructor
- Finally deploy the signPermit contract, passing the permit2 address as well.
Haven deployed the contracts, we can test visually from remix in the run tab by doing these:
- In the TestToken deployed contract, approve the Permit2 address to spend some large amount of tokens using the approve button, then proceed to mint some token to the sender’s address, using the mint button.
-
Next in the SignPermit contract section, call the function getEIP712Hash, the PermitTransferFrom should be passed as an array with its contents enclosed in double quotes to avoid errors in remix. This function returns the EIP712 typed struct hash as shown below
The argument passed to the permit parameter is an array containing a nested array of the test token address and the amount to deposit to the savings contract, and then the nonce and a timestamp in the future which we obtained by calling the getTimestamp function in the same contract. -
Now we proceed to sign this hash using the Chisel solidity REPL(Read-Evaluate-Print-Loop) which we get installed as part of the foundry installation. We can launch our terminal session and type:
$chisel
We need the private key of our sender account in remix to sign a message(or hash data), which i have gotten from this stack exchange.
Now we defined the following variables in the chisel session needed to sign the struct hash we generated from remix IDE:
- pk (the private key for our test account)
- hash (the struct hash we got from remix by calling the _getEIP712Hash function in the
SignPermit.sol contract.
- using vm.sign we compute the signature and encode with encodePacked. We can see the output of the signature which is the byte data upto 0x…1c
Haven obtained the signature, we head back to remix and call our depositERCToken function from the savings contract, passing this signature as the last argument as shown:
I’ve used 1 for the nonce argument, however any number can be used as long as it has not been used to sign a previous permit. This is made possible by the unorderd nonce schema of the permit library.
FunFact: Reading through the docs, i noticed some typo in this nonce schema section and i’ve submitted a PR to Uniswap
We observe the transactions terminal that it indeed succeeds, however to be certain that we’ve made this token transfer, we query the balance of the sender from the TestToken contract section, and we notice there’s indeed a reduction.
Now after our 1 minute withdrawal time has elapsed, we should call the withdrawToken function in the savings contract. Then once again proceed to query the token balance in the TestToken contract and we noticed there’s an increment.
Now it might seem as though we still have to send two transaction which is:
- Approving the permit2 library from the TestToken contract
- Calling the depositERC20Token from our savings contract, which uses permit2 under the hood to perform the Token transfer to the savings contract.
However, we only need to do the approval to the canonical Permit2 contract once for a given token, and if say we were to interact with say Dapp-B or any Dapp which already integrates Permit2, only one transaction would be needed for the same token transfer, (as ideally we would approve Permit2 with the max uint amount). The single transaction flow henceforth looks like this:
Permit2 on CELO
The Permit2 contract has since been deployed to CELO mainnet at this address 0x000000000022d473030f116ddee9f6b43ac78ba3, however a peek into celoscan reveals that Dapps are not yet taking advantage of this amazing contract yet as no transaction can be found on the address as at the time of this writing. Therefore, i encourage the reader to go forth and utilize the Permit2 contract in their next CELO deployment.
Conclusion
Congrats on reaching the end of this article, hopefully you’ve learned a ton and can now successfully integrate Permit2 in your Decentralized applicaion for a smoother experience for your users.
References
Permit2 docs Uniswap
EIP-712
Chisel overiew
All source code used for this tutorial can be found on the Github Gist.
About the author
Nuel is a web and blockchain engineer, with passion for building Decentralized applications, and writing well optimized smart contracts.