-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: CrosschainERC677 for the IoTeX chain
after CrosschainERC20V2 as deployed in https://iotexscan.io/address/0x1ae24d4928a86faaacd71cf414d2b3a499adb29b#code
- Loading branch information
Showing
2 changed files
with
179 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
}) | ||
}) |