From 1cc35d5c9846d0bec51e1eb29e947c9a05591f81 Mon Sep 17 00:00:00 2001 From: Juuso Takalainen Date: Wed, 27 Nov 2024 13:15:43 +0200 Subject: [PATCH 1/5] feat: CrosschainERC677 for the IoTeX chain after CrosschainERC20V2 as deployed in https://iotexscan.io/address/0x1ae24d4928a86faaacd71cf414d2b3a499adb29b#code --- contracts/CrosschainERC677.sol | 107 +++++++++++++++++++++++++++++++++ test/CrosschainERC677-test.js | 72 ++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 contracts/CrosschainERC677.sol create mode 100644 test/CrosschainERC677-test.js diff --git a/contracts/CrosschainERC677.sol b/contracts/CrosschainERC677.sol new file mode 100644 index 0000000..b19a2f3 --- /dev/null +++ b/contracts/CrosschainERC677.sol @@ -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; + } +} diff --git a/test/CrosschainERC677-test.js b/test/CrosschainERC677-test.js new file mode 100644 index 0000000..e8407a1 --- /dev/null +++ b/test/CrosschainERC677-test.js @@ -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")) + }) +}) From 58269079d801d2c6f467a0b0e9f873575028ea26 Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 23 Jan 2025 00:42:53 +0800 Subject: [PATCH 2/5] exchange legacy token for crosschain erc677 --- contracts/CrosschainERC677.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/CrosschainERC677.sol b/contracts/CrosschainERC677.sol index b19a2f3..03a851c 100644 --- a/contracts/CrosschainERC677.sol +++ b/contracts/CrosschainERC677.sol @@ -21,17 +21,20 @@ contract CrosschainERC677 is ERC20Burnable, IERC677 { _; } + ERC20Burnable public immutable legacyToken; ERC20 public coToken; address public minter; uint8 private decimals_; constructor( + ERC20Burnable _legacyToken, ERC20 _coToken, address _minter, string memory _name, string memory _symbol, uint8 _decimals ) ERC20(_name, _symbol) { + legacyToken = _legacyToken; coToken = _coToken; minter = _minter; decimals_ = _decimals; @@ -71,6 +74,11 @@ contract CrosschainERC677 is ERC20Burnable, IERC677 { coToken.safeTransfer(_to, _amount); } + function exchange(uint256 _amount) public { + require(legacyToken.burnFrom(msg.sender, _amount), "burn failed"); + _mint(msg.sender, _amount); + } + function mint(address _to, uint256 _amount) public onlyMinter returns (bool) { require(_amount != 0, "amount is 0"); _mint(_to, _amount); From 6533834e7ce1268ea10c011346443258d155a04a Mon Sep 17 00:00:00 2001 From: Juuso Takalainen Date: Mon, 27 Jan 2025 17:22:04 +0200 Subject: [PATCH 3/5] build: add ignore to lint --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index a22f0a8..c881c4e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,5 @@ module.exports = [{ - ignores: ["dist/**/*.js", "typechain/**/*.js", "index.js"], // generated files + ignores: ["dist/**/*.js", "typechain/**/*.js", "index.js", "coverage/**/*"], // generated files languageOptions: { sourceType: "commonjs", }, From b531d97aa638ca2f5aceba7db2f972c975bd82b9 Mon Sep 17 00:00:00 2001 From: Juuso Takalainen Date: Mon, 27 Jan 2025 17:22:13 +0200 Subject: [PATCH 4/5] fix: compilation error --- contracts/CrosschainERC677.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/CrosschainERC677.sol b/contracts/CrosschainERC677.sol index 03a851c..a306aef 100644 --- a/contracts/CrosschainERC677.sol +++ b/contracts/CrosschainERC677.sol @@ -75,7 +75,7 @@ contract CrosschainERC677 is ERC20Burnable, IERC677 { } function exchange(uint256 _amount) public { - require(legacyToken.burnFrom(msg.sender, _amount), "burn failed"); + legacyToken.burnFrom(msg.sender, _amount); _mint(msg.sender, _amount); } From 83e5b61180097916ae1dadc32005c7270973e101 Mon Sep 17 00:00:00 2001 From: Juuso Takalainen Date: Mon, 27 Jan 2025 17:25:02 +0200 Subject: [PATCH 5/5] test: fix changed constructor --- test/CrosschainERC677-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CrosschainERC677-test.js b/test/CrosschainERC677-test.js index e8407a1..6475660 100644 --- a/test/CrosschainERC677-test.js +++ b/test/CrosschainERC677-test.js @@ -26,7 +26,7 @@ describe("CrosschainERC677", () => { // (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) + const token = await CrosschainERC677.deploy(ZeroAddress, 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)")