Skip to content

Commit

Permalink
feat: CrosschainERC677 for the IoTeX chain
Browse files Browse the repository at this point in the history
  • Loading branch information
jtakalai committed Nov 27, 2024
1 parent b0b8674 commit 1cc35d5
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 0 deletions.
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"))
})
})

0 comments on commit 1cc35d5

Please sign in to comment.