Skip to content

Instantly share code, notes, and snippets.

@irzhywau
Last active March 27, 2022 18:18
Show Gist options
  • Save irzhywau/b1257353a2dabbf3504043d2cb52038f to your computer and use it in GitHub Desktop.
Save irzhywau/b1257353a2dabbf3504043d2cb52038f to your computer and use it in GitHub Desktop.
wrap/unwrap token inside of a contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.12;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./IERC20Wrappable.sol";
contract ERC20WrappableSupport {
modifier withWrap(address payable wToken) {
require(_bundleWrap(wToken), "failed to wrap");
_;
}
modifier withUnwrap(address payable wToken, uint256 amount) {
_;
_unwrap(wToken, amount);
}
modifier _underlyingTokenRequired(address payable wToken) {
require(wToken != address(0), "underlying token not set");
_;
}
/// @dev execute wrap transaction along the process and transfer wETH into original msg.sender account
function _bundleWrap(address payable wToken)
internal
_underlyingTokenRequired(wToken)
returns (bool)
{
// Note:
// There exists a special variant of a message `call`,
// named `delegatecall` which is identical to a message call
// apart from the fact that the code at the target address
// is executed in the context of the calling contract
// and msg.sender and msg.value do not change their values.
(bool deposited, ) = wToken.call{value: msg.value}(
abi.encodeWithSignature("deposit()")
);
require(deposited, "failed to deposit");
IERC20(wToken).transferFrom(address(this), msg.sender, msg.value);
// @todo: figure out how to bundle this in this method
// (bool approved, ) = wToken.delegatecall(
// abi.encodeWithSignature("approve(address,uint256)", this, amount)
// );
// require(approved, "failed to approve");
return true;
}
function _unwrap(address payable wToken, uint256 wad)
internal
_underlyingTokenRequired(wToken)
returns (bool)
{
// this statement needs an approval in prior
// transfer fund from user account to the current contract
require(
IERC20(wToken).transferFrom(msg.sender, address(this), wad),
"wERC20: failed to transfer to calling contract"
);
// here we will withdraw transfered wETH in the contract to ETH
// then transfer it again to the recipient (msg.sender)
// execution operated by this contract
IERC20Wrappable(wToken).withdraw(wad);
(bool withdrawn, ) = msg.sender.call{value: wad}("");
return withdrawn;
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.12;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IERC20Wrappable is IERC20 {
function deposit() external payable;
function withdraw(uint256 wad) external;
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.12;
import "@openzeppelin/contracts/access/Ownable.sol";
import "../library/ERC20WrappableSupport.sol";
contract TestWrap is Ownable, ERC20WrappableSupport {
address payable public wToken;
function pay() external payable {
require(_bundleWrap(wToken), "failed to bundle actions");
}
function refundMe(uint256 amount) external {
require(_unwrap(wToken, amount), "failed to withdraw");
}
function setWToken(address _wToken) public onlyOwner {
wToken = payable(_wToken);
}
}
@CodeMaestro11
Copy link

I can't see that you import or define IERC20Wrappable interface.
And IERC20 as well.
Maybe it seems error.

@irzhywau
Copy link
Author

I just omitted to add theme here, but it compiles correctly with all the dependencies

@CodeMaestro11
Copy link

CodeMaestro11 commented Mar 25, 2022

function withdraw(uint wad) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
msg.sender.transfer(wad);
Withdrawal(msg.sender, wad);
}

That the transaction is reverted means that it didn't passed this part " require(balanceOf[msg.sender] >= wad);".
So I recommend to check msg.sender part at withdraw function.

@malaDev
Copy link

malaDev commented Mar 26, 2022

Ive checked that part and seems ok, the error seems occurring on msg.sender.transfer instead then I dont know why it fails at that stage

@irzhywau
Copy link
Author

here is the scenario I'm trying to do, I use truffle console for it (to be able to make better debug when error occurs)
the WET9 contract code is taken from ropsten 0x0a180a76e4466bf68a7f86fb029bed3cccfaaac5

weth = await WETH9.new();
w = await TestWrap.new();

await w.setWToken(weth.address);

await w.pay({value: 20});
await weth.approve(w.address, 20);

await weth.withdraw(10); // OK
await w.refundMe(10); // revert without any explicit reason

@CodeMaestro11
Copy link

function _unwrap(address payable wToken, uint256 wad)
        internal
        _underlyingTokenRequired(wToken)
        returns (bool)
    {
        // this statement needs an approval in prior
        // transfer fund from user account to the current contract
        require(
            IERC20(wToken).transferFrom(msg.sender, address(this), wad),
            "wERC20: failed to transfer to calling contract"
        );

        // here we will withdraw transfered wETH in the contract to ETH
        // then transfer it again to the recipient (msg.sender)
        // execution operated by this contract
        IERC20Wrappable(wToken).withdraw(wad);
        (bool withdrawn, ) = msg.sender.call{value: wad}("");

        return withdrawn;
    }

Erros is in these line

        require(
            IERC20(wToken).transferFrom(msg.sender, address(this), wad),
            "wERC20: failed to transfer to calling contract"
        );

You are running transferFrom function without approve.
You need to check the allowance amount before the transferFrom function and add the approve function in test before refund function.

require(IERC20(wToken).allowance(msg.sender, address(this)) >= wad, "insufficient allowance");

I am using truffle and I will post test code.

  const { expect, use, util } = require("chai");
const { ethers } = require("hardhat");
const { solidity } = require("ethereum-waffle");
use(solidity);

describe("Eth9 Test", function () {
  let erc20Wrappable, addr1, addr2, addr3, addr4;

  beforeEach(async () => {
    [addr1, addr2, addr3, addr4] = await ethers.getSigners();
    const ERC20Wrappable = await ethers.getContractFactory("ERC20WrappableSupport");
    erc20Wrappable = await ERC20Wrappable.deploy();
    await erc20Wrappable.deployed();

    const TestWrap = await ethers.getContractFactory("TestWrap");
    testWrap = await TestWrap.deploy();
    await testWrap.deployed();

    const Weth9 = await ethers.getContractFactory("WETH9");
    weth9 = await Weth9.deploy();
    await weth9.deployed();
  });

  it("Initialize", async function () {
    expect(erc20Wrappable).to.be.ok;
    expect(testWrap).to.be.ok;
    expect(weth9).to.be.ok;
  });

  it("Token address", async function () {
    await testWrap.setWToken(weth9.address);
    expect(await testWrap.wToken()).to.equal(weth9.address);

    await testWrap.connect(addr1).pay({value: ethers.utils.parseEther("20")});
    //approve first
    await weth9.connect(addr1).approve(testWrap.address,ethers.utils.parseEther("20"));
    // next refondMe
    await testWrap.refondMe(10);
  });
});

@irzhywau
Copy link
Author

Thank you so much for your help,
finally the issue was rather about the contract that implement ERC20WrappableSupport it needed a fallback and receive functions

by adding both, everything seemed ok

but still, I'm not really sure to understand what will be the effect of that

I just added below in TestWrap

    function _fallback() internal {}

    fallback() external payable {
        _fallback();
    }

    receive() external payable {
        _fallback();
    }

@CodeMaestro11
Copy link

CodeMaestro11 commented Mar 27, 2022

receive() external payable {
        _fallback();
    }

This function means that the contract can recieve ether from ourside contract or account.
If there is no such function, we can't recieve ether from outside.

@irzhywau
Copy link
Author

got it.

thank you @alexlu0917 your help just unlocked me from a week of struggle

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment