Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ETH-794: CrosschainERC677 for the IoTeX chain #77

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions contracts/CrosschainERC677.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.6;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import "./IERC677.sol";
import "./IERC677Receiver.sol";

/**
* Version of DATAv2 adapted from IoTeX/iotube CrosschainERC20V2
* https://iotexscan.io/address/0x1ae24d4928a86faaacd71cf414d2b3a499adb29b#code
*/
contract CrosschainERC677 is ERC20Burnable, IERC677 {
using SafeERC20 for ERC20;

event MinterSet(address indexed minter);

modifier onlyMinter() {
require(minter == msg.sender, "not the minter");
_;
}

ERC20 public coToken;
address public minter;
uint8 private decimals_;

constructor(
ERC20 _coToken,
address _minter,
string memory _name,
string memory _symbol,
uint8 _decimals
) ERC20(_name, _symbol) {
coToken = _coToken;
minter = _minter;
decimals_ = _decimals;
emit MinterSet(_minter);
}

function decimals() public view virtual override returns (uint8) {
return decimals_;
}

function transferMintership(address _newMinter) public onlyMinter {
minter = _newMinter;
emit MinterSet(_newMinter);
}

function deposit(uint256 _amount) public {
depositTo(msg.sender, _amount);
}

function depositTo(address _to, uint256 _amount) public {
require(address(coToken) != address(0), "no co-token");
uint256 originBalance = coToken.balanceOf(address(this));
coToken.safeTransferFrom(msg.sender, address(this), _amount);
uint256 newBalance = coToken.balanceOf(address(this));
require(newBalance > originBalance, "invalid balance");
_mint(_to, newBalance - originBalance);
}

function withdraw(uint256 _amount) public {
withdrawTo(msg.sender, _amount);
}

function withdrawTo(address _to, uint256 _amount) public {
require(address(coToken) != address(0), "no co-token");
require(_amount != 0, "amount is 0");
_burn(msg.sender, _amount);
coToken.safeTransfer(_to, _amount);
}

function mint(address _to, uint256 _amount) public onlyMinter returns (bool) {
require(_amount != 0, "amount is 0");
_mint(_to, _amount);
return true;
}

// ------------------------------------------------------------------------
// adapted from LINK token, see https://etherscan.io/address/0x514910771af9ca656af840dff83e8264ecf986ca#code
// implements https://github.com/ethereum/EIPs/issues/677
/**
* @dev transfer token to a contract address with additional data if the recipient is a contact.
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
* @param _data The extra data to be passed to the receiving contract.
*/
function transferAndCall(
address _to,
uint256 _value,
bytes calldata _data
) public override returns (bool success) {
super.transfer(_to, _value);
emit Transfer(_msgSender(), _to, _value, _data);

uint256 recipientCodeSize;
assembly {
recipientCodeSize := extcodesize(_to)
}
if (recipientCodeSize > 0) {
IERC677Receiver receiver = IERC677Receiver(_to);
receiver.onTokenTransfer(_msgSender(), _value, _data);
}
return true;
}
}
72 changes: 72 additions & 0 deletions test/CrosschainERC677-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const { parseEther, id, ZeroAddress } = require("ethers")
const { expect } = require("chai")
const { ethers } = require("hardhat")

// "err" as bytes, induces a simulated error in MockRecipient.sol and MockRecipientReturnBool.sol
const errData = "0x657272"

describe("CrosschainERC677", () => {
it("transferAndCall triggers ERC677 callback", async () => {
const [signer, minter] = await ethers.getSigners()

const MockRecipient = await ethers.getContractFactory("MockRecipient")
const recipient = await MockRecipient.deploy()
await recipient.waitForDeployment()
const recipientAddress = await recipient.getAddress()

const MockRecipientNotERC677Receiver = await ethers.getContractFactory("MockRecipientNotERC677Receiver")
const nonReceiverRecipient = await MockRecipientNotERC677Receiver.deploy()
await nonReceiverRecipient.waitForDeployment()
const nonReceiverRecipientAddress = await nonReceiverRecipient.getAddress()

const MockRecipientReturnBool = await ethers.getContractFactory("MockRecipientReturnBool")
const returnBoolRecipient = await MockRecipientReturnBool.deploy()
await returnBoolRecipient.waitForDeployment()
const returnBoolRecipientAddress = await returnBoolRecipient.getAddress()

// (ERC20 _coToken, address _minter, string memory _name, string memory _symbol, uint8 _decimals)
const CrosschainERC677 = await ethers.getContractFactory("CrosschainERC677")
const token = await CrosschainERC677.deploy(ZeroAddress, minter.address, "TestToken", "TEST", 18)
await token.waitForDeployment()

await expect(token.connect(minter).mint(signer.address, parseEther("10"))).to.emit(token, "Transfer(address,address,uint256)")

// revert in callback => should revert transferAndCall
await expect(token.transferAndCall(recipientAddress, parseEther("1"), errData)).to.be.reverted

// no callback => should revert transferAndCall
await expect(token.transferAndCall(nonReceiverRecipientAddress, parseEther("1"), "0x")).to.be.reverted

// contract that implements ERC677Receiver executes the callback
const txsBefore = await recipient.txCount()
await token.transferAndCall(recipientAddress, parseEther("1"), "0x6c6f6c")
const txsAfter = await recipient.txCount()

// callback returns true or false but doesn't revert => should NOT revert
const txsBeforeBool = await returnBoolRecipient.txCount()
await token.transferAndCall(returnBoolRecipientAddress, parseEther("1"), errData)
await token.transferAndCall(returnBoolRecipientAddress, parseEther("1"), "0x")
const txsAfterBool = await returnBoolRecipient.txCount()

expect(txsAfter).to.equal(txsBefore + 1n)
expect(txsAfterBool).to.equal(txsBeforeBool + 2n)
})

it("transferAndCall just does normal transfer for non-contract accounts", async () => {
const [signer, minter] = await ethers.getSigners()
const targetAddress = "0x0000000000000000000000000000000000000001"

const DATAv2 = await ethers.getContractFactory("DATAv2")
const token = await DATAv2.deploy()
await token.waitForDeployment()

await expect(token.grantRole(id("MINTER_ROLE"), minter.address)).to.emit(token, "RoleGranted")
await expect(token.connect(minter).mint(signer.address, parseEther("1"))).to.emit(token, "Transfer(address,address,uint256)")

const balanceBefore = await token.balanceOf(targetAddress)
await token.transferAndCall(targetAddress, parseEther("1"), "0x6c6f6c")
const balanceAfter = await token.balanceOf(targetAddress)

expect(balanceAfter - balanceBefore).to.equal(parseEther("1"))
})
})