Skip to content

Commit ae2a6a0

Browse files
authored
Merge pull request #77 from streamr-dev/eth-794-deploy-streamr-contracts-to-iotex-chain
ETH-794: CrosschainERC677 for the IoTeX chain
2 parents 4c04a69 + 83e5b61 commit ae2a6a0

File tree

3 files changed

+188
-1
lines changed

3 files changed

+188
-1
lines changed

contracts/CrosschainERC677.sol

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.6;
3+
4+
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
7+
import "./IERC677.sol";
8+
import "./IERC677Receiver.sol";
9+
10+
/**
11+
* Version of DATAv2 adapted from IoTeX/iotube CrosschainERC20V2
12+
* https://iotexscan.io/address/0x1ae24d4928a86faaacd71cf414d2b3a499adb29b#code
13+
*/
14+
contract CrosschainERC677 is ERC20Burnable, IERC677 {
15+
using SafeERC20 for ERC20;
16+
17+
event MinterSet(address indexed minter);
18+
19+
modifier onlyMinter() {
20+
require(minter == msg.sender, "not the minter");
21+
_;
22+
}
23+
24+
ERC20Burnable public immutable legacyToken;
25+
ERC20 public coToken;
26+
address public minter;
27+
uint8 private decimals_;
28+
29+
constructor(
30+
ERC20Burnable _legacyToken,
31+
ERC20 _coToken,
32+
address _minter,
33+
string memory _name,
34+
string memory _symbol,
35+
uint8 _decimals
36+
) ERC20(_name, _symbol) {
37+
legacyToken = _legacyToken;
38+
coToken = _coToken;
39+
minter = _minter;
40+
decimals_ = _decimals;
41+
emit MinterSet(_minter);
42+
}
43+
44+
function decimals() public view virtual override returns (uint8) {
45+
return decimals_;
46+
}
47+
48+
function transferMintership(address _newMinter) public onlyMinter {
49+
minter = _newMinter;
50+
emit MinterSet(_newMinter);
51+
}
52+
53+
function deposit(uint256 _amount) public {
54+
depositTo(msg.sender, _amount);
55+
}
56+
57+
function depositTo(address _to, uint256 _amount) public {
58+
require(address(coToken) != address(0), "no co-token");
59+
uint256 originBalance = coToken.balanceOf(address(this));
60+
coToken.safeTransferFrom(msg.sender, address(this), _amount);
61+
uint256 newBalance = coToken.balanceOf(address(this));
62+
require(newBalance > originBalance, "invalid balance");
63+
_mint(_to, newBalance - originBalance);
64+
}
65+
66+
function withdraw(uint256 _amount) public {
67+
withdrawTo(msg.sender, _amount);
68+
}
69+
70+
function withdrawTo(address _to, uint256 _amount) public {
71+
require(address(coToken) != address(0), "no co-token");
72+
require(_amount != 0, "amount is 0");
73+
_burn(msg.sender, _amount);
74+
coToken.safeTransfer(_to, _amount);
75+
}
76+
77+
function exchange(uint256 _amount) public {
78+
legacyToken.burnFrom(msg.sender, _amount);
79+
_mint(msg.sender, _amount);
80+
}
81+
82+
function mint(address _to, uint256 _amount) public onlyMinter returns (bool) {
83+
require(_amount != 0, "amount is 0");
84+
_mint(_to, _amount);
85+
return true;
86+
}
87+
88+
// ------------------------------------------------------------------------
89+
// adapted from LINK token, see https://etherscan.io/address/0x514910771af9ca656af840dff83e8264ecf986ca#code
90+
// implements https://github.com/ethereum/EIPs/issues/677
91+
/**
92+
* @dev transfer token to a contract address with additional data if the recipient is a contact.
93+
* @param _to The address to transfer to.
94+
* @param _value The amount to be transferred.
95+
* @param _data The extra data to be passed to the receiving contract.
96+
*/
97+
function transferAndCall(
98+
address _to,
99+
uint256 _value,
100+
bytes calldata _data
101+
) public override returns (bool success) {
102+
super.transfer(_to, _value);
103+
emit Transfer(_msgSender(), _to, _value, _data);
104+
105+
uint256 recipientCodeSize;
106+
assembly {
107+
recipientCodeSize := extcodesize(_to)
108+
}
109+
if (recipientCodeSize > 0) {
110+
IERC677Receiver receiver = IERC677Receiver(_to);
111+
receiver.onTokenTransfer(_msgSender(), _value, _data);
112+
}
113+
return true;
114+
}
115+
}

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = [{
2-
ignores: ["dist/**/*.js", "typechain/**/*.js", "index.js"], // generated files
2+
ignores: ["dist/**/*.js", "typechain/**/*.js", "index.js", "coverage/**/*"], // generated files
33
languageOptions: {
44
sourceType: "commonjs",
55
},

test/CrosschainERC677-test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const { parseEther, id, ZeroAddress } = require("ethers")
2+
const { expect } = require("chai")
3+
const { ethers } = require("hardhat")
4+
5+
// "err" as bytes, induces a simulated error in MockRecipient.sol and MockRecipientReturnBool.sol
6+
const errData = "0x657272"
7+
8+
describe("CrosschainERC677", () => {
9+
it("transferAndCall triggers ERC677 callback", async () => {
10+
const [signer, minter] = await ethers.getSigners()
11+
12+
const MockRecipient = await ethers.getContractFactory("MockRecipient")
13+
const recipient = await MockRecipient.deploy()
14+
await recipient.waitForDeployment()
15+
const recipientAddress = await recipient.getAddress()
16+
17+
const MockRecipientNotERC677Receiver = await ethers.getContractFactory("MockRecipientNotERC677Receiver")
18+
const nonReceiverRecipient = await MockRecipientNotERC677Receiver.deploy()
19+
await nonReceiverRecipient.waitForDeployment()
20+
const nonReceiverRecipientAddress = await nonReceiverRecipient.getAddress()
21+
22+
const MockRecipientReturnBool = await ethers.getContractFactory("MockRecipientReturnBool")
23+
const returnBoolRecipient = await MockRecipientReturnBool.deploy()
24+
await returnBoolRecipient.waitForDeployment()
25+
const returnBoolRecipientAddress = await returnBoolRecipient.getAddress()
26+
27+
// (ERC20 _coToken, address _minter, string memory _name, string memory _symbol, uint8 _decimals)
28+
const CrosschainERC677 = await ethers.getContractFactory("CrosschainERC677")
29+
const token = await CrosschainERC677.deploy(ZeroAddress, ZeroAddress, minter.address, "TestToken", "TEST", 18)
30+
await token.waitForDeployment()
31+
32+
await expect(token.connect(minter).mint(signer.address, parseEther("10"))).to.emit(token, "Transfer(address,address,uint256)")
33+
34+
// revert in callback => should revert transferAndCall
35+
await expect(token.transferAndCall(recipientAddress, parseEther("1"), errData)).to.be.reverted
36+
37+
// no callback => should revert transferAndCall
38+
await expect(token.transferAndCall(nonReceiverRecipientAddress, parseEther("1"), "0x")).to.be.reverted
39+
40+
// contract that implements ERC677Receiver executes the callback
41+
const txsBefore = await recipient.txCount()
42+
await token.transferAndCall(recipientAddress, parseEther("1"), "0x6c6f6c")
43+
const txsAfter = await recipient.txCount()
44+
45+
// callback returns true or false but doesn't revert => should NOT revert
46+
const txsBeforeBool = await returnBoolRecipient.txCount()
47+
await token.transferAndCall(returnBoolRecipientAddress, parseEther("1"), errData)
48+
await token.transferAndCall(returnBoolRecipientAddress, parseEther("1"), "0x")
49+
const txsAfterBool = await returnBoolRecipient.txCount()
50+
51+
expect(txsAfter).to.equal(txsBefore + 1n)
52+
expect(txsAfterBool).to.equal(txsBeforeBool + 2n)
53+
})
54+
55+
it("transferAndCall just does normal transfer for non-contract accounts", async () => {
56+
const [signer, minter] = await ethers.getSigners()
57+
const targetAddress = "0x0000000000000000000000000000000000000001"
58+
59+
const DATAv2 = await ethers.getContractFactory("DATAv2")
60+
const token = await DATAv2.deploy()
61+
await token.waitForDeployment()
62+
63+
await expect(token.grantRole(id("MINTER_ROLE"), minter.address)).to.emit(token, "RoleGranted")
64+
await expect(token.connect(minter).mint(signer.address, parseEther("1"))).to.emit(token, "Transfer(address,address,uint256)")
65+
66+
const balanceBefore = await token.balanceOf(targetAddress)
67+
await token.transferAndCall(targetAddress, parseEther("1"), "0x6c6f6c")
68+
const balanceAfter = await token.balanceOf(targetAddress)
69+
70+
expect(balanceAfter - balanceBefore).to.equal(parseEther("1"))
71+
})
72+
})

0 commit comments

Comments
 (0)