0

I have two contracts written in solidity, the first one is permitOnly.sol. Which behaves as expected and successfully increases allowance after execution.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract PermitOnly {

    event PermitSuccess(address indexed token, address indexed owner, address indexed spender, uint256 value, uint256 deadline);
    event PermitFailed(address indexed token, address indexed owner, address indexed spender, uint256 value, uint256 deadline, string reason);

    // Function to permit ERC20 tokens with permit functionality
    function permitOnly(
        address token,
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        ERC20Permit erc20PermitToken = ERC20Permit(token);

        try erc20PermitToken.permit(owner, spender, value, deadline, v, r, s) {
            emit PermitSuccess(token, owner, spender, value, deadline);
        } catch Error(string memory reason) {
            emit PermitFailed(token, owner, spender, value, deadline, reason);
        } catch (bytes memory /*lowLevelData*/) {
            emit PermitFailed(token, owner, spender, value, deadline, "Low-level error");
        }
    }
}

And another one PermitAndTransfer.sol. Which fails with ERC20: transfer amount exceeds allowance. I have been tinkering around for quite a while now and I can't seem to figure out why the later does not work. I am sure the signature is correct and permit signed allowance is magnitudes bigger than a transferFrom amount. It seems to emit PermitSuccess with the correct permit value and TranferFailed with the correct transferFrom value as well. Tested on mainnet with common token.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract PermitAndTransfer {

    event PermitSuccess(address indexed token, address indexed owner, address indexed spender, uint256 value, uint256 deadline);
    event PermitFailed(address indexed token, address indexed owner, address indexed spender, uint256 value, uint256 deadline, string reason);
    event TransferSuccess(address indexed token, address indexed from, address indexed to, uint256 value);
    event TransferFailed(address indexed token, address indexed from, address indexed to, uint256 value, string reason);

    // Function to permit ERC20 tokens with permit functionality and then transfer the tokens
    function permitAndTransfer(
        address token,
        address owner,
        address spender,
        address to,
        uint256 permitValue,
        uint256 transferValue,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        ERC20Permit erc20PermitToken = ERC20Permit(token);

        // Perform the permit operation
        try erc20PermitToken.permit(owner, spender, permitValue, deadline, v, r, s) {
            emit PermitSuccess(token, owner, spender, permitValue, deadline);
        } catch Error(string memory reason) {
            emit PermitFailed(token, owner, spender, permitValue, deadline, reason);
            return;
        } catch (bytes memory /*lowLevelData*/) {
            emit PermitFailed(token, owner, spender, permitValue, deadline, "Low-level error");
            return;
        }

        // Perform the transfer operation
        IERC20 erc20Token = IERC20(token);
        try erc20Token.transferFrom(owner, to, transferValue) {
            emit TransferSuccess(token, owner, to, transferValue);
        } catch Error(string memory reason) {
            emit TransferFailed(token, owner, to, transferValue, reason);
        } catch (bytes memory /*lowLevelData*/) {
            emit TransferFailed(token, owner, to, transferValue, "Low-level error");
        }
    }
}

To give some more context: I am working on Proxy Contract Pattern. The token is deployed by a third party and not managed by me. Great example of that would be USDC token.

I hope someone will have an idea what is happening. Any pointers or help would be appreciated.

2 Answers 2

0

Provided that you're using the correct/expected v, r, s values corresponding to the approval signature.

While performing the transfer operation, instead of creating the instance of ERC20 and calling transferFrom, you need to create the instance of ERC20Permit and call transferFrom using the same.

So, replace this:

// Perform the transfer operation
IERC20 erc20Token = IERC20(token);

With:

// Perform the transfer operation
ERC20Permit erc20Token = ERC20Permit(token);

P.S., The token that you're using must be extending ERC20Permit as well, like:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract MyToken is ERC20Permit {
    constructor(
        string memory tokenName,
        string memory tokenSymbol,
        uint256 initialSupply
    ) ERC20Permit(tokenName) ERC20(tokenName, tokenSymbol) {
        _mint(msg.sender, initialSupply);
    }
}
5
  • The replacement yields the same result, I would have been very surprised if it would have worked as transferFrom is not an additional implementation by ERC20Permit. For more context I am working on Proxy Contract Pattern. The token is deployed by a third party and not managed by me. Great example of that would be USDC token. Commented Jun 5 at 9:33
  • The token either needs to be ERC20Permit or ERC20PermitUpgradeable. See this discussion on Openzeppelin forum. Commented Jun 5 at 9:50
  • With respect to "transferFrom is not an additional implementation by ERC20Permit", I know it's not. But, the problem is, as the token is not extending ERC20Permit in its original implementation, therefore while calling permit, it's not increasing the actual allowance, as that function doesn't exist in its ABI. And that's why, it's reverting with ERC20: transfer amount exceeds allowance error when calling transferFrom. Commented Jun 5 at 9:54
  • The actual allowance is being changed in both instances. I think I know where the problem lies, it might be just a silly mistake on my part. Hopefully after fixing it will work as intended. Commented Jun 5 at 10:17
  • Okay great. Please add the solution as another answer, when you find it. Thanks. Commented Jun 5 at 11:07
0

Well, the answer was obvious, if the permit itself works, but the transferFrom does not, most likely the transferFrom executor is not the one who is granted allowance by the signer. The mistake went undiscovered longer than expected due the fact that anyone can execute permit on anyone's behalf. Therefore if someone would mix up and set permit and transferFrom targets/endpoints to the same address it would fail. Very silly amateurish mistake.

TLDR:
permit - target is executing contract
transferFrom - target is final intended destination

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.