diff --git a/contracts/iotube/TokenCashierV3.sol b/contracts/iotube/TokenCashierV3.sol new file mode 100644 index 0000000..49ac90c --- /dev/null +++ b/contracts/iotube/TokenCashierV3.sol @@ -0,0 +1,124 @@ +pragma solidity <6.0 >=0.4.24; + +import "../lifecycle/Pausable.sol"; + +interface ITokenList { + function isAllowed(address) external returns (bool); + function maxAmount(address) external returns (uint256); + function minAmount(address) external returns (uint256); +} + +interface IWrappedCoin { + function deposit() external payable; +} + +contract TokenCashierV3 is Pausable { + event Receipt(address indexed token, uint256 indexed id, address sender, address recipient, uint256 amount, uint256 fee, bytes payload); + // event ContractDestinationAdded(address indexed destination); + // event ContractDestinationRemoved(address indexed destination); + + ITokenList[] public tokenLists; + address[] public tokenSafes; + mapping(address => uint256) public counts; + uint256 public depositFee; + IWrappedCoin public wrappedCoin; + // mapping(address => bool) public contractDestinations; + + constructor(IWrappedCoin _wrappedCoin, ITokenList[] memory _tokenLists, address[] memory _tokenSafes) public { + require(_tokenLists.length == _tokenSafes.length, "# of token lists is not equal to # of safes"); + wrappedCoin = _wrappedCoin; + tokenLists = _tokenLists; + tokenSafes = _tokenSafes; + } + + function() external { + revert(); + } + + function count(address _token) public view returns (uint256) { + return counts[_token]; + } +/* + function addContractDestination(address _dest) public onlyOwner { + require(!contractDestinations[_dest], "already added"); + contractDestinations[_dest] = true; + emit ContractDestinationAdded(_dest); + } + + function removeContractDestination(address _dest) public onlyOwner { + require(contractDestinations[_dest], "invalid destination"); + contractDestinations[_dest] = false; + emit ContractDestinationRemoved(_dest); + } +*/ + function setDepositFee(uint256 _fee) public onlyOwner { + depositFee = _fee; + } + + function depositTo(address _token, address _to, uint256 _amount, bytes memory _payload) public whenNotPaused payable { + require(_to != address(0), "invalid destination"); + // require(_payload.length == 0 || contractDestinations[_to], "invalid destination with payload"); + bool isCoin = false; + uint256 fee = msg.value; + if (_token == address(0)) { + require(msg.value >= _amount, "insufficient msg.value"); + fee = msg.value - _amount; + wrappedCoin.deposit.value(_amount)(); + _token = address(wrappedCoin); + isCoin = true; + } + require(fee >= depositFee, "insufficient fee"); + for (uint256 i = 0; i < tokenLists.length; i++) { + if (tokenLists[i].isAllowed(_token)) { + require(_amount >= tokenLists[i].minAmount(_token), "amount too low"); + require(_amount <= tokenLists[i].maxAmount(_token), "amount too high"); + if (tokenSafes[i] == address(0)) { + require(!isCoin && safeTransferFrom(_token, msg.sender, address(this), _amount), "fail to transfer token to cashier"); + // selector = bytes4(keccak256(bytes('burn(uint256)'))) + (bool success, bytes memory data) = _token.call(abi.encodeWithSelector(0x42966c68, _amount)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "fail to burn token"); + } else { + if (isCoin) { + require(safeTransfer(_token, tokenSafes[i], _amount), "failed to put into safe"); + } else { + require(safeTransferFrom(_token, msg.sender, tokenSafes[i], _amount), "failed to put into safe"); + } + } + counts[_token] += 1; + emit Receipt(_token, counts[_token], msg.sender, _to, _amount, fee, _payload); + return; + } + } + revert("not a whitelisted token"); + } + + function deposit(address _token, uint256 _amount, bytes memory _payload) public payable { + depositTo(_token, msg.sender, _amount, _payload); + } + + function withdraw() external onlyOwner { + msg.sender.transfer(address(this).balance); + } + + function withdrawToken(address _token) public onlyOwner { + // selector = bytes4(keccak256(bytes('balanceOf(address)'))) + (bool success, bytes memory balance) = _token.call(abi.encodeWithSelector(0x70a08231, address(this))); + require(success, "failed to call balanceOf"); + uint256 bal = abi.decode(balance, (uint256)); + if (bal > 0) { + require(safeTransfer(_token, msg.sender, bal), "failed to withdraw token"); + } + } + + function safeTransferFrom(address _token, address _from, address _to, uint256 _amount) internal returns (bool) { + // selector = bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))) + (bool success, bytes memory data) = _token.call(abi.encodeWithSelector(0x23b872dd, _from, _to, _amount)); + return success && (data.length == 0 || abi.decode(data, (bool))); + } + + function safeTransfer(address _token, address _to, uint256 _amount) internal returns (bool) { + // selector = bytes4(keccak256(bytes('transfer(address,uint256)'))) + (bool success, bytes memory data) = _token.call(abi.encodeWithSelector(0xa9059cbb, _to, _amount)); + return success && (data.length == 0 || abi.decode(data, (bool))); + } +} \ No newline at end of file diff --git a/contracts/iotube/TransferValidatorV3.sol b/contracts/iotube/TransferValidatorV3.sol new file mode 100644 index 0000000..2d87435 --- /dev/null +++ b/contracts/iotube/TransferValidatorV3.sol @@ -0,0 +1,135 @@ +pragma solidity <6.0 >=0.4.24; + +import "../lifecycle/Pausable.sol"; + +interface IAllowlist { + function isAllowed(address) external view returns (bool); + function numOfActive() external view returns (uint256); +} + +interface IMinter { + function mint(address, address, uint256) external returns(bool); + function transferOwnership(address) external; + function owner() external view returns(address); +} + +interface IReceiver { + function onReceive(bytes32 id, address sender, address token, uint256 amount, bytes calldata payload) external; +} + +contract TransferValidatorV3 is Pausable { + event Settled(bytes32 indexed key, address[] witnesses); + event ReceiverAdded(address receiver); + event ReceiverRemoved(address receiver); + + mapping(bytes32 => uint256) public settles; + mapping(address => bool) public receivers; + + IMinter[] public minters; + IAllowlist[] public tokenLists; + IAllowlist public witnessList; + + constructor(IAllowlist _witnessList) public { + witnessList = _witnessList; + } + + function generateKey(address cashier, address tokenAddr, uint256 index, address from, address to, uint256 amount, bytes memory payload) public view returns(bytes32) { + return keccak256(abi.encodePacked(address(this), cashier, tokenAddr, index, from, to, amount, payload)); + } + + function submit(address cashier, address tokenAddr, uint256 index, address from, address to, uint256 amount, bytes memory signatures, bytes memory payload) public whenNotPaused { + require(amount != 0, "amount cannot be zero"); + require(to != address(0), "recipient cannot be zero"); + require(signatures.length % 65 == 0, "invalid signature length"); + bytes32 key = generateKey(cashier, tokenAddr, index, from, to, amount, payload); + require(settles[key] == 0, "transfer has been settled"); + for (uint256 it = 0; it < tokenLists.length; it++) { + if (tokenLists[it].isAllowed(tokenAddr)) { + uint256 numOfSignatures = signatures.length / 65; + address[] memory witnesses = new address[](numOfSignatures); + for (uint256 i = 0; i < numOfSignatures; i++) { + address witness = recover(key, signatures, i * 65); + require(witnessList.isAllowed(witness), "invalid signature"); + for (uint256 j = 0; j < i; j++) { + require(witness != witnesses[j], "duplicate witness"); + } + witnesses[i] = witness; + } + require(numOfSignatures * 3 > witnessList.numOfActive() * 2, "insufficient witnesses"); + settles[key] = block.number; + require(minters[it].mint(tokenAddr, to, amount), "failed to mint token"); + if (receivers[to]) { + IReceiver(to).onReceive(key, from, tokenAddr, amount, payload); + } + emit Settled(key, witnesses); + return; + } + } + revert("not a whitelisted token"); + } + + function numOfPairs() external view returns (uint256) { + return tokenLists.length; + } + + function addPair(IAllowlist _tokenList, IMinter _minter) external onlyOwner { + tokenLists.push(_tokenList); + minters.push(_minter); + } + + function addReceiver(address _receiver) external onlyOwner { + require(!receivers[_receiver], "already a receiver"); + receivers[_receiver] = true; + emit ReceiverAdded(_receiver); + } + + function removeReceiver(address _receiver) external onlyOwner { + require(receivers[_receiver], "invalid receiver"); + receivers[_receiver] = false; + emit ReceiverRemoved(_receiver); + } + + function upgrade(address _newValidator) external onlyOwner { + address contractAddr = address(this); + for (uint256 i = 0; i < minters.length; i++) { + IMinter minter = minters[i]; + if (minter.owner() == contractAddr) { + minter.transferOwnership(_newValidator); + } + } + } + + /** + * @dev Recover signer address from a message by using their signature + * @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address. + * @param signature bytes signature, the signature is generated using web3.eth.sign() + */ + function recover(bytes32 hash, bytes memory signature, uint256 offset) + internal + pure + returns (address) + { + bytes32 r; + bytes32 s; + uint8 v; + + // Divide the signature in r, s and v variables with inline assembly. + assembly { + r := mload(add(signature, add(offset, 0x20))) + s := mload(add(signature, add(offset, 0x40))) + v := byte(0, mload(add(signature, add(offset, 0x60)))) + } + + // Version of signature should be 27 or 28, but 0 and 1 are also possible versions + if (v < 27) { + v += 27; + } + + // If the version is correct return the signer address + if (v != 27 && v != 28) { + return (address(0)); + } + // solium-disable-next-line arg-overflow + return ecrecover(hash, v, r, s); + } +} diff --git a/scripts/alter_transfer_table.sh b/scripts/alter_transfer_table.sh index ab36568..bf59d1b 100644 --- a/scripts/alter_transfer_table.sh +++ b/scripts/alter_transfer_table.sh @@ -1,10 +1,10 @@ -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.bsc_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.ethereum_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.heco_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.matic_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.polis_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_heco_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_polis_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_bsc_transfers ADD txSender varchar(42);" -docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_matic_transfers ADD txSender varchar(42);" \ No newline at end of file +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.bsc_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.ethereum_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.heco_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.matic_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.polis_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_heco_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_polis_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_bsc_transfers ADD payload varchar(24576);" +docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_matic_transfers ADD payload varchar(24576);" \ No newline at end of file diff --git a/witness-service/cmd/relayer/main.go b/witness-service/cmd/relayer/main.go index 47059e0..d5d4425 100644 --- a/witness-service/cmd/relayer/main.go +++ b/witness-service/cmd/relayer/main.go @@ -35,17 +35,18 @@ import ( // Configuration defines the configuration of the witness service type Configuration struct { - Chain string `json:"chain" yaml:"chain"` - ClientURL string `json:"clientURL" yaml:"clientURL"` - EthConfirmBlockNumber uint16 `json:"ethConfirmBlockNumber" yaml:"ethConfirmBlockNumber"` - EthDefaultGasPrice uint64 `json:"ethDefaultGasPrice" yaml:"ethDefaultGasPrice"` - EthGasPriceLimit uint64 `json:"ethGasPriceLimit" yaml:"ethGasPriceLimit"` - EthGasPriceHardLimit uint64 `json:"ethGasPriceHardLimit" yaml:"ethGasPriceHardLimit"` - EthGasPriceDeviation int64 `json:"ethGasPriceDeviation" yaml:"ethGasPriceDeviation"` - EthGasPriceGap uint64 `json:"ethGasPriceGap" yaml:"ethGasPriceGap"` - PrivateKey string `json:"privateKey" yaml:"privateKey"` - Interval time.Duration `json:"interval" yaml:"interval"` - ValidatorAddress string `json:"vialidatorAddress" yaml:"validatorAddress"` + Chain string `json:"chain" yaml:"chain"` + ClientURL string `json:"clientURL" yaml:"clientURL"` + EthConfirmBlockNumber uint16 `json:"ethConfirmBlockNumber" yaml:"ethConfirmBlockNumber"` + EthDefaultGasPrice uint64 `json:"ethDefaultGasPrice" yaml:"ethDefaultGasPrice"` + EthGasPriceLimit uint64 `json:"ethGasPriceLimit" yaml:"ethGasPriceLimit"` + EthGasPriceHardLimit uint64 `json:"ethGasPriceHardLimit" yaml:"ethGasPriceHardLimit"` + EthGasPriceDeviation int64 `json:"ethGasPriceDeviation" yaml:"ethGasPriceDeviation"` + EthGasPriceGap uint64 `json:"ethGasPriceGap" yaml:"ethGasPriceGap"` + PrivateKey string `json:"privateKey" yaml:"privateKey"` + Interval time.Duration `json:"interval" yaml:"interval"` + Version relayer.Version `json:"version" yaml:"version"` + ValidatorAddress string `json:"vialidatorAddress" yaml:"validatorAddress"` BonusTokens map[string]*big.Int `json:"bonusTokens" yaml:"bonusTokens"` Bonus *big.Int `json:"bonus" yaml:"bonus"` @@ -75,6 +76,7 @@ var defaultConfig = Configuration{ PrivateKey: "", SlackWebHook: "", LarkWebHook: "", + Version: relayer.V1, TransferTableName: "relayer.transfers", WitnessTableName: "relayer.witnesses", } @@ -171,6 +173,7 @@ func main() { new(big.Int).SetUint64(cfg.EthGasPriceHardLimit), new(big.Int).SetInt64(cfg.EthGasPriceDeviation), new(big.Int).SetUint64(cfg.EthGasPriceGap), + cfg.Version, common.HexToAddress(cfg.ValidatorAddress), ); err != nil { log.Fatalf("failed to create transfer validator: %v\n", err) @@ -196,6 +199,7 @@ func main() { } if transferValidator, err = relayer.NewTransferValidatorOnIoTeX( iotex.NewAuthedClient(iotexapi.NewAPIServiceClient(conn), 1, acc), + cfg.Version, validatorContractAddr, cfg.BonusTokens, cfg.Bonus, diff --git a/witness-service/cmd/witness/main.go b/witness-service/cmd/witness/main.go index 460e49b..1caf7cf 100644 --- a/witness-service/cmd/witness/main.go +++ b/witness-service/cmd/witness/main.go @@ -31,6 +31,12 @@ import ( "github.com/iotexproject/ioTube/witness-service/witness" ) +// TokenPair defines a token pair +type TokenPair struct { + Token1 string `json:"token1" yaml:"token1"` + Token2 string `json:"token2" yaml:"token2"` +} + // Configuration defines the configuration of the witness service type Configuration struct { Chain string `json:"chain" yaml:"chain"` @@ -47,18 +53,16 @@ type Configuration struct { GrpcProxyPort int `json:"grpcProxyPort" yaml:"grpcProxyPort"` DisableTransferSubmit bool `json:"disableTransferSubmit" yaml:"disableTransferSubmit"` Cashiers []struct { - ID string `json:"id" yaml:"id"` - RelayerURL string `json:"relayerURL" yaml:"relayerURL"` - CashierContractAddress string `json:"cashierContractAddress" yaml:"cashierContractAddress"` - TokenSafeContractAddress string `json:"tokenSafeContractAddress" yaml:"tokenSafeContractAddress"` - ValidatorContractAddress string `json:"vialidatorContractAddress" yaml:"validatorContractAddress"` - TransferTableName string `json:"transferTableName" yaml:"transferTableName"` - TokenPairs []struct { - Token1 string `json:"token1" yaml:"token1"` - Token2 string `json:"token2" yaml:"token2"` - } `json:"tokenPairs" yaml:"tokenPairs"` - StartBlockHeight int `json:"startBlockHeight" yaml:"startBlockHeight"` - Reverse struct { + ID string `json:"id" yaml:"id"` + RelayerURL string `json:"relayerURL" yaml:"relayerURL"` + Version witness.Version `json:"version" yaml:"version"` + CashierContractAddress string `json:"cashierContractAddress" yaml:"cashierContractAddress"` + TokenSafeContractAddress string `json:"tokenSafeContractAddress" yaml:"tokenSafeContractAddress"` + ValidatorContractAddress string `json:"vialidatorContractAddress" yaml:"validatorContractAddress"` + TransferTableName string `json:"transferTableName" yaml:"transferTableName"` + TokenPairs []TokenPair `json:"tokenPairs" yaml:"tokenPairs"` + StartBlockHeight int `json:"startBlockHeight" yaml:"startBlockHeight"` + Reverse struct { TransferTableName string `json:"transferTableName" yaml:"transferTableName"` CashierContractAddress string `json:"cashierContractAddress" yaml:"cashierContractAddress"` Tokens []string `json:"tokens" yaml:"tokens"` @@ -95,6 +99,37 @@ func init() { } } +func parseAddress(addr string) (common.Address, error) { + if strings.HasPrefix(addr, "io") { + ioAddr, err := address.FromString(addr) + if err != nil { + log.Fatalf("failed to parse iotex address %s, %v\n", addr, err) + } + return common.BytesToAddress(ioAddr.Bytes()), nil + } + return common.HexToAddress(addr), nil +} + +func parseTokenPairs(tokenPairs []TokenPair) map[common.Address]common.Address { + pairs := make(map[common.Address]common.Address) + for _, pair := range tokenPairs { + token1, err := parseAddress(pair.Token1) + if err != nil { + log.Fatalf("failed to parse token1 address %s, %v\n", pair.Token1, err) + } + if _, ok := pairs[token1]; ok { + log.Fatalf("duplicate token key %s\n", pair.Token1) + } + token2, err := parseAddress(pair.Token2) + if err != nil { + log.Fatalf("failed to parse token2 address %s, %v\n", pair.Token1, err) + } + pairs[token1] = token2 + } + + return pairs +} + func main() { flag.Parse() opts := []config.YAMLOption{config.Static(defaultConfig), config.Expand(os.LookupEnv)} @@ -176,19 +211,9 @@ func main() { if err != nil { log.Fatalf("failed to parse cashier contract address %s, %v\n", cc.CashierContractAddress, err) } - pairs := make(map[common.Address]common.Address) - for _, pair := range cc.TokenPairs { - ioAddr, err := address.FromString(pair.Token1) - if err != nil { - log.Fatalf("failed to parse iotex address %s, %v\n", pair.Token1, err) - } - if _, ok := pairs[common.BytesToAddress(ioAddr.Bytes())]; ok { - log.Fatalf("duplicate token key %s\n", pair.Token1) - } - pairs[common.BytesToAddress(ioAddr.Bytes())] = common.HexToAddress(pair.Token2) - } cashier, err := witness.NewTokenCashier( cc.ID, + cc.Version, cc.RelayerURL, iotexClient, cashierContractAddr, @@ -196,7 +221,7 @@ func main() { witness.NewRecorder( db.NewStore(cfg.Database), cc.TransferTableName, - pairs, + parseTokenPairs(cc.TokenPairs), ), uint64(cc.StartBlockHeight), ) @@ -243,6 +268,7 @@ func main() { } cashier, err := witness.NewTokenCashierOnEthereum( cc.ID, + cc.Version, cc.RelayerURL, ethClient, common.HexToAddress(cc.CashierContractAddress), @@ -251,7 +277,7 @@ func main() { witness.NewRecorder( db.NewStore(cfg.Database), cc.TransferTableName, - pairs, + parseTokenPairs(cc.TokenPairs), ), uint64(cc.StartBlockHeight), uint8(cfg.ConfirmBlockNumber), diff --git a/witness-service/contract/abigen.sh b/witness-service/contract/abigen.sh index e7b0749..2a87d07 100755 --- a/witness-service/contract/abigen.sh +++ b/witness-service/contract/abigen.sh @@ -16,3 +16,5 @@ abigen --abi $DIR/TokenCashier.abi --bin $DIR/TokenCashier.bin --pkg contract -- abigen --abi $DIR/TokenList.abi --bin $DIR/TokenList.bin --pkg contract --type TokenList --out $DIR/tokenlist.go abigen --abi $DIR/ShadowToken.abi --bin $DIR/ShadowToken.bin --pkg contract --type ShadowToken --out $DIR/shadowtoken.go abigen --abi $DIR/addresslist.abi --bin $DIR/addresslist.bin --pkg contract --type AddressList --out $DIR/addresslist.go +abigen --abi $DIR/TransferValidatorV3.abi --pkg contract --type TransferValidatorV3 --out $DIR/transfervaldiatorv3.go +abigen --abi $DIR/TokenCashierV3.abi --pkg contract --type TokenCashierV3 --out $DIR/tokencashierv3.go diff --git a/witness-service/grpc/types/witness.pb.go b/witness-service/grpc/types/witness.pb.go index d0f1b3b..1da5d24 100644 --- a/witness-service/grpc/types/witness.pb.go +++ b/witness-service/grpc/types/witness.pb.go @@ -37,6 +37,7 @@ type Transfer struct { GasPrice string `protobuf:"bytes,9,opt,name=gasPrice,proto3" json:"gasPrice,omitempty"` Fee string `protobuf:"bytes,10,opt,name=fee,proto3" json:"fee,omitempty"` TxSender []byte `protobuf:"bytes,11,opt,name=txSender,proto3" json:"txSender,omitempty"` + Payload []byte `protobuf:"bytes,12,opt,name=payload,proto3" json:"payload,omitempty"` } func (x *Transfer) Reset() { @@ -148,6 +149,13 @@ func (x *Transfer) GetTxSender() []byte { return nil } +func (x *Transfer) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + type Witness struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -217,7 +225,7 @@ var file_types_witness_proto_rawDesc = []byte{ 0x0a, 0x13, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x77, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb4, 0x02, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xce, 0x02, 0x0a, 0x08, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x61, 0x73, 0x68, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x61, 0x73, 0x68, 0x69, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, @@ -237,18 +245,20 @@ var file_types_witness_proto_rawDesc = []byte{ 0x73, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x65, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x65, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x78, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x74, 0x78, 0x53, 0x65, - 0x6e, 0x64, 0x65, 0x72, 0x22, 0x6e, 0x0a, 0x07, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x12, - 0x2b, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, - 0x65, 0x72, 0x52, 0x08, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x69, 0x6f, 0x74, 0x65, 0x78, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, - 0x69, 0x6f, 0x54, 0x75, 0x62, 0x65, 0x2f, 0x77, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x2d, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x6e, + 0x0a, 0x07, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x12, 0x2b, 0x0a, 0x08, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x66, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x08, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x42, 0x3b, + 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6f, 0x74, + 0x65, 0x78, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6f, 0x54, 0x75, 0x62, 0x65, + 0x2f, 0x77, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/witness-service/proto/types/witness.proto b/witness-service/proto/types/witness.proto index eac0993..7ef1bf8 100644 --- a/witness-service/proto/types/witness.proto +++ b/witness-service/proto/types/witness.proto @@ -18,6 +18,7 @@ message Transfer { string gasPrice = 9; string fee = 10; bytes txSender = 11; + bytes payload = 12; } message Witness { diff --git a/witness-service/relayer/recorder.go b/witness-service/relayer/recorder.go index 4d4de05..c0fc3b6 100644 --- a/witness-service/relayer/recorder.go +++ b/witness-service/relayer/recorder.go @@ -30,6 +30,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/iotexproject/ioTube/witness-service/db" + "github.com/iotexproject/ioTube/witness-service/util" ) type ( @@ -235,7 +236,7 @@ func (recorder *Recorder) addWitness( transferTableName, witnessTableName string, ) error { if _, err := tx.Exec( - fmt.Sprintf("INSERT IGNORE INTO %s (cashier, token, tidx, sender, txSender, recipient, amount, fee, id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", transferTableName), + fmt.Sprintf("INSERT IGNORE INTO %s (cashier, token, tidx, sender, txSender, recipient, amount, payload, fee, id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", transferTableName), transfer.cashier.Hex(), transfer.token.Hex(), transfer.index, @@ -243,6 +244,7 @@ func (recorder *Recorder) addWitness( transfer.txSender.Hex(), transfer.recipient.Hex(), transfer.amount.String(), + util.EncodeToNullString(transfer.payload), transfer.fee.String(), transfer.id.Hex(), ); err != nil { @@ -318,7 +320,7 @@ func (recorder *Recorder) assembleTransfer(scan func(dest ...interface{}) error) tx := &Transfer{} var rawAmount string var cashier, token, sender, recipient, id string - var relayer, hash, gasPrice, fee, txSender sql.NullString + var relayer, hash, payload, gasPrice, fee, txSender sql.NullString var gas, nonce sql.NullInt64 var timestamp sql.NullTime if err := scan(&cashier, &token, &tx.index, &sender, &txSender, &recipient, &rawAmount, &fee, &id, &hash, ×tamp, &nonce, &gas, &gasPrice, &tx.status, &tx.updateTime, &relayer); err != nil { @@ -348,6 +350,11 @@ func (recorder *Recorder) assembleTransfer(scan func(dest ...interface{}) error) if timestamp.Valid { tx.timestamp = timestamp.Time } + var err error + tx.payload, err = util.DecodeNullString(payload) + if err != nil { + return nil, err + } tx.fee = big.NewInt(0) var ok bool if fee.Valid { @@ -374,7 +381,7 @@ func (recorder *Recorder) Transfer(id common.Hash) (*Transfer, error) { recorder.mutex.RLock() defer recorder.mutex.RUnlock() row := recorder.store.DB().QueryRow( - fmt.Sprintf("SELECT `cashier`, `token`, `tidx`, `sender`, `txSender`, `recipient`, `amount`, `fee`, `id`, `txHash`, `txTimestamp`, `nonce`, `gas`, `gasPrice`, `status`, `updateTime`, `relayer` FROM %s WHERE `id`=?", recorder.transferTableName), + fmt.Sprintf("SELECT `cashier`, `token`, `tidx`, `sender`, `txSender`, `recipient`, `amount`, `payload`, `fee`, `id`, `txHash`, `txTimestamp`, `nonce`, `gas`, `gasPrice`, `status`, `updateTime`, `relayer` FROM %s WHERE `id`=?", recorder.transferTableName), id.Hex(), ) return recorder.assembleTransfer(row.Scan) @@ -482,7 +489,7 @@ func (recorder *Recorder) Transfers( if byUpdateTime { orderBy = "updateTime" } - query = fmt.Sprintf("SELECT `cashier`, `token`, `tidx`, `sender`, `txSender`, `recipient`, `amount`, `fee`, `id`, `txHash`, `txTimestamp`, `nonce`, `gas`, `gasPrice`, `status`, `updateTime`, `relayer` FROM %s", recorder.transferTableName) + query = fmt.Sprintf("SELECT `cashier`, `token`, `tidx`, `sender`, `txSender`, `recipient`, `amount`, `payload`, `fee`, `id`, `txHash`, `txTimestamp`, `nonce`, `gas`, `gasPrice`, `status`, `updateTime`, `relayer` FROM %s", recorder.transferTableName) params := []interface{}{} queryOpts = append(queryOpts, ExcludeAmountZeroOption()) conditions := []string{} diff --git a/witness-service/relayer/transfervalidatoronethereum.go b/witness-service/relayer/transfervalidatoronethereum.go index 2c367bd..cbae7eb 100644 --- a/witness-service/relayer/transfervalidatoronethereum.go +++ b/witness-service/relayer/transfervalidatoronethereum.go @@ -27,6 +27,12 @@ import ( var zeroAddress = common.Address{} +// Version +type Version string + +const V1 Version = "V1" +const V3 Version = "V3" + // transferValidatorOnEthereum defines the transfer validator type transferValidatorOnEthereum struct { mu sync.RWMutex @@ -39,6 +45,7 @@ type transferValidatorOnEthereum struct { chainID *big.Int privateKeys []*ecdsa.PrivateKey + version Version validatorContractAddr common.Address client *ethclient.Client @@ -57,6 +64,7 @@ func NewTransferValidatorOnEthereum( gasPriceHardLimit *big.Int, gasPriceDeviation *big.Int, gasPriceGap *big.Int, + version Version, validatorContractAddr common.Address, ) (TransferValidator, error) { validatorContract, err := contract.NewTransferValidator(validatorContractAddr, client) @@ -81,6 +89,7 @@ func NewTransferValidatorOnEthereum( chainID: chainID, privateKeys: privateKeys, + version: version, validatorContractAddr: validatorContractAddr, client: client, diff --git a/witness-service/relayer/transfervalidatoroniotex.go b/witness-service/relayer/transfervalidatoroniotex.go index 83f59e1..ef90378 100644 --- a/witness-service/relayer/transfervalidatoroniotex.go +++ b/witness-service/relayer/transfervalidatoroniotex.go @@ -22,6 +22,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/ioTube/witness-service/contract" "github.com/iotexproject/ioTube/witness-service/util" "github.com/iotexproject/iotex-address/address" @@ -41,10 +42,11 @@ type transferValidatorOnIoTeX struct { relayerAddr address.Address validatorContractAddr address.Address + version Version client iotex.AuthedClient validatorContract iotex.Contract - validatorContractABI abi.ABI + unpack func(name string, data []byte) ([]interface{}, error) witnessListContract iotex.Contract witnessListContractABI abi.ABI witnesses map[string]bool @@ -53,6 +55,7 @@ type transferValidatorOnIoTeX struct { // NewTransferValidatorOnIoTeX creates a new TransferValidator on IoTeX func NewTransferValidatorOnIoTeX( client iotex.AuthedClient, + version Version, validatorContractAddr address.Address, bonusTokens map[string]*big.Int, bonus *big.Int, @@ -61,7 +64,13 @@ func NewTransferValidatorOnIoTeX( if err != nil { return nil, err } - validatorABI, err := abi.JSON(strings.NewReader(contract.TransferValidatorABI)) + var validatorABI abi.ABI + switch version { + case V1: + validatorABI, err = abi.JSON(strings.NewReader(contract.TransferValidatorABI)) + case V3: + validatorABI, err = abi.JSON(strings.NewReader(contract.TransferValidatorV3ABI)) + } if err != nil { return nil, err } @@ -98,10 +107,11 @@ func NewTransferValidatorOnIoTeX( relayerAddr: client.Account().Address(), validatorContractAddr: validatorContractIoAddr, + version: version, client: client, validatorContract: validatorContract, - validatorContractABI: validatorABI, + unpack: validatorABI.Unpack, witnessListContract: client.Contract(witnessContractIoAddr, witnessContractABI), witnessListContractABI: witnessContractABI, }, nil @@ -189,7 +199,7 @@ func (tv *transferValidatorOnIoTeX) Check(transfer *Transfer) (StatusOnChainType if err != nil { return StatusOnChainUnknown, err } - ret, err := tv.validatorContractABI.Unpack("settles", settleHeightData.Raw) + ret, err := tv.unpack("settles", settleHeightData.Raw) if err != nil { return StatusOnChainUnknown, err } @@ -315,19 +325,38 @@ func (tv *transferValidatorOnIoTeX) submit(transfer *Transfer, witnesses []*Witn nonce = accountMeta.PendingNonce } - actionHash, err := tv.validatorContract.Execute( - "submit", - transfer.cashier, - transfer.token, - new(big.Int).SetUint64(transfer.index), - transfer.sender, - transfer.recipient, - transfer.amount, - signatures, - ).SetGasPrice(tv.gasPrice). - SetGasLimit(tv.gasLimit). - SetNonce(nonce). - Call(context.Background()) + var actionHash hash.Hash256 + switch tv.version { + case V1: + actionHash, err = tv.validatorContract.Execute( + "submit", + transfer.cashier, + transfer.token, + new(big.Int).SetUint64(transfer.index), + transfer.sender, + transfer.recipient, + transfer.amount, + signatures, + ).SetGasPrice(tv.gasPrice). + SetGasLimit(tv.gasLimit). + SetNonce(nonce). + Call(context.Background()) + case V3: + actionHash, err = tv.validatorContract.Execute( + "submit", + transfer.cashier, + transfer.token, + new(big.Int).SetUint64(transfer.index), + transfer.sender, + transfer.recipient, + transfer.amount, + transfer.payload, + signatures, + ).SetGasPrice(tv.gasPrice). + SetGasLimit(tv.gasLimit). + SetNonce(nonce). + Call(context.Background()) + } if err != nil { if errors.Cause(err).Error() == "rpc error: code = Internal desc = exceeds block gas limit" { err = errors.Wrap(errNoncritical, err.Error()) diff --git a/witness-service/relayer/types.go b/witness-service/relayer/types.go index 24d7b55..5fc112d 100644 --- a/witness-service/relayer/types.go +++ b/witness-service/relayer/types.go @@ -32,6 +32,7 @@ type ( txSender common.Address recipient common.Address amount *big.Int + payload []byte fee *big.Int id common.Hash txHash common.Hash @@ -126,6 +127,7 @@ func UnmarshalTransferProto(validatorAddr common.Address, transfer *types.Transf sender.Bytes(), recipient.Bytes(), math.U256Bytes(amount), + transfer.Payload, ) return &Transfer{ @@ -141,6 +143,7 @@ func UnmarshalTransferProto(validatorAddr common.Address, transfer *types.Transf gasPrice: gasPrice, timestamp: transfer.Timestamp.AsTime(), txSender: txSender, + payload: transfer.Payload, }, nil } @@ -178,12 +181,14 @@ func (transfer *Transfer) ToTypesTransfer() *types.Transfer { Token: transfer.token.Bytes(), Index: int64(transfer.index), Sender: transfer.sender.Bytes(), + TxSender: transfer.txSender.Bytes(), Recipient: transfer.recipient.Bytes(), Amount: transfer.amount.String(), Fee: transfer.fee.String(), Gas: transfer.gas, GasPrice: gasPrice, Timestamp: timestamppb.New(transfer.timestamp), + Payload: transfer.payload, } } diff --git a/witness-service/witness/recorder.go b/witness-service/witness/recorder.go index d942ecd..8af7bce 100644 --- a/witness-service/witness/recorder.go +++ b/witness-service/witness/recorder.go @@ -23,6 +23,7 @@ import ( "github.com/pkg/errors" "github.com/iotexproject/ioTube/witness-service/db" + "github.com/iotexproject/ioTube/witness-service/util" ) type ( @@ -71,6 +72,7 @@ func (recorder *Recorder) Start(ctx context.Context) error { "`sender` varchar(42) NOT NULL,"+ "`recipient` varchar(42) NOT NULL,"+ "`amount` varchar(78) NOT NULL,"+ + "`payload` varchar(24576),"+ // MaxCodeSize "`fee` varchar(78),"+ "`creationTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,"+ "`updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"+ @@ -111,7 +113,7 @@ func (recorder *Recorder) AddTransfer(tx *Transfer, status TransferStatus) error if tx.amount.Sign() != 1 { return errors.New("amount should be larger than 0") } - query := fmt.Sprintf("INSERT IGNORE INTO %s (`cashier`, `token`, `tidx`, `sender`, `recipient`, `amount`, `fee`, `blockHeight`, `txHash`, `txSender`, `status`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", recorder.transferTableName) + query := fmt.Sprintf("INSERT IGNORE INTO %s (`cashier`, `token`, `tidx`, `sender`, `recipient`, `amount`, `payload`, `fee`, `blockHeight`, `txHash`, `txSender`, `status`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", recorder.transferTableName) result, err := recorder.store.DB().Exec( query, tx.cashier.Hex(), @@ -120,6 +122,7 @@ func (recorder *Recorder) AddTransfer(tx *Transfer, status TransferStatus) error tx.sender.Hex(), tx.recipient.Hex(), tx.amount.String(), + util.EncodeToNullString(tx.payload), tx.fee.String(), tx.blockHeight, tx.txHash.Hex(), @@ -146,7 +149,7 @@ func (recorder *Recorder) UpsertTransfer(tx *Transfer) error { if tx.amount.Sign() != 1 { return errors.New("amount should be larger than 0") } - query := fmt.Sprintf("INSERT INTO %s (`cashier`, `token`, `tidx`, `sender`, `recipient`, `amount`, `fee`, `blockHeight`, `txHash`, `txSender`, `status`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE `status` = IF(status = ?, ?, status)", recorder.transferTableName) + query := fmt.Sprintf("INSERT INTO %s (`cashier`, `token`, `tidx`, `sender`, `recipient`, `amount`, `payload`, `fee`, `blockHeight`, `txHash`, `txSender`, `status`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE `status` = IF(status = ?, ?, status)", recorder.transferTableName) result, err := recorder.store.DB().Exec( query, tx.cashier.Hex(), @@ -155,6 +158,7 @@ func (recorder *Recorder) UpsertTransfer(tx *Transfer) error { tx.sender.Hex(), tx.recipient.Hex(), tx.amount.String(), + util.EncodeToNullString(tx.payload), tx.fee.String(), tx.blockHeight, tx.txHash.Hex(), @@ -173,8 +177,8 @@ func (recorder *Recorder) UpsertTransfer(tx *Transfer) error { if affected == 0 { log.Printf("duplicate transfer (%s, %s, %d) ignored\n", tx.cashier.Hex(), tx.token.Hex(), tx.index) } - return nil + return nil } func (recorder *Recorder) AmountOfTransferred(cashier, token common.Address) (*big.Int, error) { @@ -273,7 +277,7 @@ func (recorder *Recorder) TransfersToSubmit() ([]*Transfer, error) { func (recorder *Recorder) transfers(status TransferStatus) ([]*Transfer, error) { rows, err := recorder.store.DB().Query( fmt.Sprintf( - "SELECT cashier, token, tidx, sender, recipient, amount, fee, status, id, txSender "+ + "SELECT cashier, token, tidx, sender, recipient, amount, payload, fee, status, id, txSender "+ "FROM %s "+ "WHERE status=? "+ "ORDER BY creationTime", @@ -296,8 +300,9 @@ func (recorder *Recorder) transfers(status TransferStatus) ([]*Transfer, error) var rawAmount string var fee sql.NullString var id sql.NullString + var payload sql.NullString var txSender sql.NullString - if err := rows.Scan(&cashier, &token, &tx.index, &sender, &recipient, &rawAmount, &fee, &tx.status, &id, &txSender); err != nil { + if err := rows.Scan(&cashier, &token, &tx.index, &sender, &recipient, &rawAmount, &payload, &fee, &tx.status, &id, &txSender); err != nil { return nil, err } tx.cashier = common.HexToAddress(cashier) @@ -318,6 +323,10 @@ func (recorder *Recorder) transfers(status TransferStatus) ([]*Transfer, error) return nil, errors.Errorf("invalid fee %s", fee.String) } } + tx.payload, err = util.DecodeNullString(payload) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode payload %s", payload) + } tx.amount, ok = new(big.Int).SetString(rawAmount, 10) if !ok || tx.amount.Sign() != 1 { return nil, errors.Errorf("invalid amount %s", rawAmount) @@ -335,7 +344,7 @@ func (recorder *Recorder) transfers(status TransferStatus) ([]*Transfer, error) func (recorder *Recorder) Transfer(_id common.Hash) (*Transfer, error) { row := recorder.store.DB().QueryRow( - fmt.Sprintf("SELECT `cashier`, `token`, `tidx`, `sender`, `recipient`, `amount`, `status`, `id`, `txSender` FROM %s WHERE `id`=?", recorder.transferTableName), + fmt.Sprintf("SELECT `cashier`, `token`, `tidx`, `sender`, `recipient`, `amount`, `payload`, `status`, `id`, `txSender` FROM %s WHERE `id`=?", recorder.transferTableName), _id.Hex(), ) @@ -346,8 +355,9 @@ func (recorder *Recorder) Transfer(_id common.Hash) (*Transfer, error) { var recipient string var rawAmount string var id sql.NullString + var payload sql.NullString var txSender sql.NullString - if err := row.Scan(&cashier, &token, &tx.index, &sender, &recipient, &rawAmount, &tx.status, &id, &txSender); err != nil { + if err := row.Scan(&cashier, &token, &tx.index, &sender, &recipient, &rawAmount, &payload, &tx.status, &id, &txSender); err != nil { return nil, err } @@ -366,6 +376,11 @@ func (recorder *Recorder) Transfer(_id common.Hash) (*Transfer, error) { if txSender.Valid { tx.txSender = common.HexToAddress(txSender.String) } + var err error + tx.payload, err = util.DecodeNullString(payload) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode payload %s", payload) + } if toToken, ok := recorder.tokenPairs[tx.token]; ok { tx.coToken = toToken } else { diff --git a/witness-service/witness/service.go b/witness-service/witness/service.go index d8ce382..2409680 100644 --- a/witness-service/witness/service.go +++ b/witness-service/witness/service.go @@ -95,6 +95,7 @@ func (s *service) sign(transfer *Transfer, validatorContractAddr common.Address) transfer.sender.Bytes(), transfer.recipient.Bytes(), math.U256Bytes(transfer.amount), + transfer.payload, ) if s.privateKey == nil { return id, common.Address{}, nil, nil @@ -198,6 +199,7 @@ func (s *service) Query(ctx context.Context, request *services.QueryRequest) (*s Timestamp: timestamppb.New(tx.timestamp), Gas: tx.gas, GasPrice: gasPrice, + Payload: tx.payload, }, } diff --git a/witness-service/witness/tokencashierbase.go b/witness-service/witness/tokencashierbase.go index 44446f7..086ad7b 100644 --- a/witness-service/witness/tokencashierbase.go +++ b/witness-service/witness/tokencashierbase.go @@ -24,6 +24,7 @@ import ( var ( _ReceiptEventTopic, _TransferEventTopic common.Hash + _ReceiptEventTopicV3 common.Hash _ZeroHash = common.Hash{} ) @@ -33,6 +34,11 @@ func init() { log.Panicf("failed to decode token cashier abi, %+v", err) } _ReceiptEventTopic = tokenCashierABI.Events["Receipt"].ID + tokenCashierV3ABI, err := abi.JSON(strings.NewReader(contract.TokenCashierV3ABI)) + if err != nil { + log.Panicf("failed to decode token cashier abi, %+v", err) + } + _ReceiptEventTopicV3 = tokenCashierV3ABI.Events["Receipt"].ID erc20ABI, err := abi.JSON(strings.NewReader(contract.CrosschainERC20ABI)) if err != nil { log.Panicf("failed to decode erc20 abi, %+v", err) @@ -233,6 +239,7 @@ func (tc *tokenCashierBase) SubmitTransfers(sign func(*Transfer, common.Address) Amount: transfer.amount.String(), Fee: transfer.fee.String(), TxSender: transfer.txSender.Bytes(), + Payload: transfer.payload, }, Address: witness.Bytes(), Signature: signature, diff --git a/witness-service/witness/tokencashieronethereum.go b/witness-service/witness/tokencashieronethereum.go index aaa7d92..4749687 100644 --- a/witness-service/witness/tokencashieronethereum.go +++ b/witness-service/witness/tokencashieronethereum.go @@ -7,21 +7,183 @@ package witness import ( - "bytes" "context" "log" "math/big" - "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" + "github.com/iotexproject/ioTube/witness-service/contract" "github.com/pkg/errors" ) +type iterator struct { + version Version + client *ethclient.Client + cashierContractAddr common.Address + tokenSafeContractAddr common.Address +} + +func newIterator(version Version, cashierContractAddr, tokenSafeContractAddr common.Address, client *ethclient.Client) (*iterator, error) { + iter := &iterator{ + version: version, + cashierContractAddr: cashierContractAddr, + tokenSafeContractAddr: tokenSafeContractAddr, + client: client, + } + var err error + switch version { + case V1: + default: + return nil, errors.Errorf("invalid version %s", version) + } + if err != nil { + return nil, err + } + return iter, nil +} + +func (iter *iterator) extract( + tokenAddress, senderAddress, recipient common.Address, + index uint64, + amount, fee *big.Int, + payload []byte, + raw types.Log, +) (*Transfer, error) { + receipt, err := iter.client.TransactionReceipt(context.Background(), raw.TxHash) + if err != nil { + return nil, err + } + var realAmount *big.Int + for _, l := range receipt.Logs { + if l.Address == tokenAddress && l.Topics[0] == _TransferEventTopic && (l.Topics[1] == senderAddress.Hash() || l.Topics[1] == raw.Address.Hash()) { + if l.Topics[2] == iter.cashierContractAddr.Hash() || l.Topics[2] != _ZeroHash && l.Topics[2] == iter.tokenSafeContractAddr.Hash() { + if realAmount != nil { + return nil, errors.Errorf("two transfers in one transaction %x", raw.TxHash) + } + realAmount = new(big.Int).SetBytes(l.Data) + } + } + } + if realAmount == nil { + return nil, errors.Errorf("failed to get the amount from transfer event for %x", raw.TxHash) + } + switch realAmount.Cmp(amount) { + case 1: + return nil, errors.Errorf("Invalid amount: %d < %d", amount, realAmount) + case -1: + log.Printf("\tAmount %d is reduced %d after tax\n", amount, realAmount) + case 0: + log.Printf("\tAmount %d is the same as real amount %d\n", amount, realAmount) + } + tx, err := iter.client.TransactionInBlock(context.Background(), raw.BlockHash, raw.TxIndex) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch transaction") + } + from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx) + if err != nil { + return nil, errors.Wrap(err, "failed to extract sender") + } + tsf := &Transfer{ + cashier: raw.Address, + token: tokenAddress, + index: index, + sender: senderAddress, + recipient: recipient, + amount: amount, + fee: fee, + blockHeight: raw.BlockNumber, + txHash: raw.TxHash, + payload: payload, + } + if from != senderAddress { + tsf.txSender = from + } + + return tsf, nil +} + +func (iter *iterator) Transfers(start, end uint64) ([]*Transfer, error) { + transfers := []*Transfer{} + switch iter.version { + case V1: + filter, err := contract.NewTokenCashierFilterer(iter.cashierContractAddr, iter.client) + if err != nil { + return nil, err + } + iterator, err := filter.FilterReceipt( + &bind.FilterOpts{ + Start: start, + End: &end, + }, + nil, + nil, + ) + if err != nil { + return nil, err + } + for iterator.Next() { + tsf, err := iter.extract( + iterator.Event.Token, + iterator.Event.Sender, + iterator.Event.Recipient, + iterator.Event.Id.Uint64(), + iterator.Event.Amount, + iterator.Event.Fee, + nil, + iterator.Event.Raw, + ) + if err != nil { + return nil, err + } + transfers = append(transfers, tsf) + } + case V3: + filter, err := contract.NewTokenCashierV3Filterer(iter.cashierContractAddr, iter.client) + if err != nil { + return nil, err + } + iterator, err := filter.FilterReceipt( + &bind.FilterOpts{ + Start: start, + End: &end, + }, + nil, + nil, + ) + if err != nil { + return nil, err + } + for iterator.Next() { + tsf, err := iter.extract( + iterator.Event.Token, + iterator.Event.Sender, + iterator.Event.Recipient, + iterator.Event.Id.Uint64(), + iterator.Event.Amount, + iterator.Event.Fee, + iterator.Event.Payload, + iterator.Event.Raw, + ) + if err != nil { + return nil, err + } + transfers = append(transfers, tsf) + + } + default: + return nil, errors.New("invalid version") + } + + return transfers, nil +} + // NewTokenCashierOnEthereum creates a new TokenCashier on ethereum func NewTokenCashierOnEthereum( id string, + version Version, relayerURL string, ethereumClient *ethclient.Client, cashierContractAddr common.Address, @@ -33,6 +195,10 @@ func NewTokenCashierOnEthereum( reverseRecorder *Recorder, reverseCashierContractAddr common.Address, ) (TokenCashier, error) { + iter, err := newIterator(version, cashierContractAddr, tokenSafeContractAddr, ethereumClient) + if err != nil { + return nil, err + } return newTokenCashierBase( id, recorder, @@ -57,83 +223,7 @@ func NewTokenCashierOnEthereum( } return tipHeight - uint64(confirmBlockNumber), endHeight, nil }, - func(startHeight uint64, endHeight uint64) ([]*Transfer, error) { - logs, err := ethereumClient.FilterLogs(context.Background(), ethereum.FilterQuery{ - FromBlock: new(big.Int).SetUint64(startHeight), - ToBlock: new(big.Int).SetUint64(endHeight), - Addresses: []common.Address{cashierContractAddr}, - Topics: [][]common.Hash{ - { - _ReceiptEventTopic, - }, - }, - }) - if err != nil { - return nil, err - } - transfers := []*Transfer{} - if len(logs) > 0 { - log.Printf("\t%d transfers fetched from %d to %d\n", len(logs), startHeight, endHeight) - for _, transferLog := range logs { - if !bytes.Equal(_ReceiptEventTopic[:], transferLog.Topics[0][:]) { - return nil, errors.Errorf("Wrong event topic %x, %x expected", transferLog.Topics[0], _ReceiptEventTopic) - } - tokenAddress := common.BytesToAddress(transferLog.Topics[1][:]) - senderAddress := common.BytesToAddress(transferLog.Data[:32]) - amount := new(big.Int).SetBytes(transferLog.Data[64:96]) - receipt, err := ethereumClient.TransactionReceipt(context.Background(), transferLog.TxHash) - if err != nil { - return nil, err - } - var realAmount *big.Int - for _, l := range receipt.Logs { - if l.Address == tokenAddress && l.Topics[0] == _TransferEventTopic && (l.Topics[1] == senderAddress.Hash() || l.Topics[1] == transferLog.Address.Hash()) { - if l.Topics[2] == cashierContractAddr.Hash() || l.Topics[2] != _ZeroHash && l.Topics[2] == tokenSafeContractAddr.Hash() { - if realAmount != nil { - return nil, errors.Errorf("two transfers in one transaction %x", transferLog.TxHash) - } - realAmount = new(big.Int).SetBytes(l.Data) - } - } - } - if realAmount == nil { - return nil, errors.Errorf("failed to get the amount from transfer event for %x", transferLog.TxHash) - } - switch realAmount.Cmp(amount) { - case 1: - return nil, errors.Errorf("Invalid amount: %d < %d", amount, realAmount) - case -1: - log.Printf("\tAmount %d is reduced %d after tax\n", amount, realAmount) - case 0: - log.Printf("\tAmount %d is the same as real amount %d\n", amount, realAmount) - } - tx, err := ethereumClient.TransactionInBlock(context.Background(), transferLog.BlockHash, transferLog.TxIndex) - if err != nil { - return nil, errors.Wrap(err, "failed to fetch transaction") - } - from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx) - if err != nil { - return nil, errors.Wrap(err, "failed to extract sender") - } - tsf := &Transfer{ - cashier: transferLog.Address, - token: tokenAddress, - index: new(big.Int).SetBytes(transferLog.Topics[2][:]).Uint64(), - sender: senderAddress, - recipient: common.BytesToAddress(transferLog.Data[32:64]), - amount: amount, - fee: new(big.Int).SetBytes(transferLog.Data[96:128]), - blockHeight: transferLog.BlockNumber, - txHash: transferLog.TxHash, - } - if from != senderAddress { - tsf.txSender = from - } - transfers = append(transfers, tsf) - } - } - return transfers, nil - }, + iter.Transfers, func(token common.Address, amountToTransfer *big.Int) bool { if reverseRecorder == nil { return true diff --git a/witness-service/witness/tokencashieroniotex.go b/witness-service/witness/tokencashieroniotex.go index 4e8c414..bae0474 100644 --- a/witness-service/witness/tokencashieroniotex.go +++ b/witness-service/witness/tokencashieroniotex.go @@ -7,7 +7,6 @@ package witness import ( - "bytes" "context" "encoding/hex" "log" @@ -17,12 +16,135 @@ import ( "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-antenna-go/v2/iotex" "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" "github.com/pkg/errors" ) +type iotexIterator struct { + version Version + client iotex.ReadOnlyClient + cashierContractAddr address.Address +} + +func (ii *iotexIterator) filterLogs(topic []byte, startHeight, endHeight uint64) (*iotexapi.GetLogsResponse, error) { + return ii.client.API().GetLogs(context.Background(), &iotexapi.GetLogsRequest{ + Filter: &iotexapi.LogsFilter{ + Address: []string{ii.cashierContractAddr.String()}, + Topics: []*iotexapi.Topics{ + { + Topic: [][]byte{topic}, + }, + }, + }, + Lookup: &iotexapi.GetLogsRequest_ByRange{ + ByRange: &iotexapi.GetLogsByRange{ + FromBlock: startHeight, + // TODO: this is a bug, which should be fixed in iotex-core + ToBlock: endHeight, + }, + }, + }) +} + +func (ii *iotexIterator) extractTransfer( + transferLog *iotextypes.Log, + topic common.Hash, +) (*Transfer, error) { + senderAddr := common.BytesToAddress(transferLog.Data[:32]) + amount := new(big.Int).SetBytes(transferLog.Data[64:96]) + + receipt, err := ii.client.API().GetReceiptByAction(context.Background(), &iotexapi.GetReceiptByActionRequest{ + ActionHash: hex.EncodeToString(transferLog.ActHash), + }) + if err != nil { + return nil, err + } + tokenAddr := common.BytesToAddress(transferLog.Topics[1]) + tokenIoAddr, err := address.FromBytes(tokenAddr.Bytes()) + if err != nil { + return nil, err + } + cashierAddr, err := address.FromString(transferLog.ContractAddress) + if err != nil { + return nil, err + } + cashier := common.BytesToAddress(cashierAddr.Bytes()) + var realAmount *big.Int + for _, l := range receipt.ReceiptInfo.Receipt.Logs { + if tokenIoAddr.String() == l.ContractAddress && common.BytesToHash(l.Topics[0]) == topic && (common.BytesToAddress(l.Topics[1]) == senderAddr || cashier == common.BytesToAddress(l.Topics[1])) { + if realAmount != nil && common.BytesToHash(l.Topics[2]) != _ZeroHash { + return nil, errors.Errorf("two transfers in one transaction %x", transferLog.ActHash) + } + realAmount = new(big.Int).SetBytes(l.Data) + } + } + if realAmount == nil { + return nil, errors.Errorf("failed to get the amount from transfer event for %x", transferLog.ActHash) + } + switch realAmount.Cmp(amount) { + case 1: + return nil, errors.Errorf("Invalid amount: %d < %d", amount, realAmount) + case -1: + log.Printf("\tAmount %d is reduced %d after tax\n", amount, realAmount) + case 0: + log.Printf("\tAmount %d is the same as real amount %d\n", amount, realAmount) + } + + return &Transfer{ + cashier: cashier, + token: tokenAddr, + index: new(big.Int).SetBytes(transferLog.Topics[2]).Uint64(), + sender: senderAddr, + recipient: common.BytesToAddress(transferLog.Data[32:64]), + amount: amount, + fee: new(big.Int).SetBytes(transferLog.Data[96:128]), + blockHeight: transferLog.BlkHeight, + txHash: common.BytesToHash(transferLog.ActHash), + payload: transferLog.Data[128:], + }, nil +} + +func (ii *iotexIterator) Transfers(startHeight uint64, endHeight uint64) ([]*Transfer, error) { + transfers := []*Transfer{} + switch ii.version { + case V1: + response, err := ii.filterLogs(_ReceiptEventTopic[:], startHeight, endHeight) + if err != nil { + return nil, err + } + for _, transferLog := range response.Logs { + if len(transferLog.Data) != 128 { + return nil, errors.Errorf("Invalid data length %d, 128 expected", len(transferLog.Data)) + } + tsf, err := ii.extractTransfer(transferLog, _ReceiptEventTopic) + if err != nil { + return nil, err + } + transfers = append(transfers, tsf) + } + case V3: + response, err := ii.filterLogs(_ReceiptEventTopicV3[:], startHeight, endHeight) + if err != nil { + return nil, err + } + for _, transferLog := range response.Logs { + if len(transferLog.Data) >= 128 { + return nil, errors.Errorf("Invalid data length %d < 128", len(transferLog.Data)) + } + tsf, err := ii.extractTransfer(transferLog, _ReceiptEventTopicV3) + if err != nil { + return nil, err + } + transfers = append(transfers, tsf) + } + } + return transfers, nil +} + // NewTokenCashier creates a new TokenCashier func NewTokenCashier( id string, + version Version, relayerURL string, iotexClient iotex.ReadOnlyClient, cashierContractAddr address.Address, @@ -30,6 +152,11 @@ func NewTokenCashier( recorder *Recorder, startBlockHeight uint64, ) (TokenCashier, error) { + iter := &iotexIterator{ + version: version, + client: iotexClient, + cashierContractAddr: cashierContractAddr, + } return newTokenCashierBase( id, recorder, @@ -54,93 +181,7 @@ func NewTokenCashier( } return endHeight, endHeight, nil }, - func(startHeight uint64, endHeight uint64) ([]*Transfer, error) { - response, err := iotexClient.API().GetLogs(context.Background(), &iotexapi.GetLogsRequest{ - Filter: &iotexapi.LogsFilter{ - Address: []string{cashierContractAddr.String()}, - Topics: []*iotexapi.Topics{ - { - Topic: [][]byte{ - _ReceiptEventTopic.Bytes(), - }, - }, - }, - }, - Lookup: &iotexapi.GetLogsRequest_ByRange{ - ByRange: &iotexapi.GetLogsByRange{ - FromBlock: startHeight, - // TODO: this is a bug, which should be fixed in iotex-core - ToBlock: endHeight, - }, - }, - }) - if err != nil { - return nil, err - } - transfers := []*Transfer{} - if len(response.Logs) > 0 { - log.Printf("\t%d transfers fetched from %d to %d\n", len(response.Logs), startHeight, endHeight) - for _, transferLog := range response.Logs { - if !bytes.Equal(_ReceiptEventTopic.Bytes(), transferLog.Topics[0]) { - return nil, errors.Errorf("Wrong event topic %s, %s expected", transferLog.Topics[0], _ReceiptEventTopic) - } - if len(transferLog.Data) != 128 { - return nil, errors.Errorf("Invalid data length %d, 128 expected", len(transferLog.Data)) - } - senderAddr := common.BytesToAddress(transferLog.Data[:32]) - amount := new(big.Int).SetBytes(transferLog.Data[64:96]) - receipt, err := iotexClient.API().GetReceiptByAction(context.Background(), &iotexapi.GetReceiptByActionRequest{ - ActionHash: hex.EncodeToString(transferLog.ActHash), - }) - if err != nil { - return nil, err - } - tokenAddr := common.BytesToAddress(transferLog.Topics[1]) - tokenIoAddr, err := address.FromBytes(tokenAddr.Bytes()) - if err != nil { - return nil, err - } - cashierAddr, err := address.FromString(transferLog.ContractAddress) - if err != nil { - return nil, err - } - cashier := common.BytesToAddress(cashierAddr.Bytes()) - var realAmount *big.Int - for _, l := range receipt.ReceiptInfo.Receipt.Logs { - if tokenIoAddr.String() == l.ContractAddress && common.BytesToHash(l.Topics[0]) == _TransferEventTopic && (common.BytesToAddress(l.Topics[1]) == senderAddr || cashier == common.BytesToAddress(l.Topics[1])) { - if realAmount != nil && common.BytesToHash(l.Topics[2]) != _ZeroHash { - return nil, errors.Errorf("two transfers in one transaction %x", transferLog.ActHash) - } - realAmount = new(big.Int).SetBytes(l.Data) - } - } - if realAmount == nil { - return nil, errors.Errorf("failed to get the amount from transfer event for %x", transferLog.ActHash) - } - switch realAmount.Cmp(amount) { - case 1: - return nil, errors.Errorf("Invalid amount: %d < %d", amount, realAmount) - case -1: - log.Printf("\tAmount %d is reduced %d after tax\n", amount, realAmount) - case 0: - log.Printf("\tAmount %d is the same as real amount %d\n", amount, realAmount) - } - - transfers = append(transfers, &Transfer{ - cashier: cashier, - token: tokenAddr, - index: new(big.Int).SetBytes(transferLog.Topics[2]).Uint64(), - sender: senderAddr, - recipient: common.BytesToAddress(transferLog.Data[32:64]), - amount: amount, - fee: new(big.Int).SetBytes(transferLog.Data[96:128]), - blockHeight: transferLog.BlkHeight, - txHash: common.BytesToHash(transferLog.ActHash), - }) - } - } - return transfers, nil - }, + iter.Transfers, func(common.Address, *big.Int) bool { return true }, diff --git a/witness-service/witness/types.go b/witness-service/witness/types.go index b3e8fec..3641145 100644 --- a/witness-service/witness/types.go +++ b/witness-service/witness/types.go @@ -15,6 +15,8 @@ import ( ) type ( + // Version + Version string // TransferStatus is the status of a transfer TransferStatus string @@ -36,6 +38,7 @@ type ( gas uint64 gasPrice *big.Int txSender common.Address + payload []byte } // Service manages to exchange iotex coin to ERC20 token on ethereum @@ -69,4 +72,9 @@ const ( SubmissionConfirmed = "confirmed" // TransferSettled stands for a settled transfer TransferSettled = "settled" + + // V1 is version 1.0 + V1 Version = "v1.0" + // V3 is version 3.0 + V3 Version = "v3.0" )