Skip to content

Commit

Permalink
eth/client: add transfer_erc20 function (#197)
Browse files Browse the repository at this point in the history
* spec/fixtures: add erc20 stub

* eth/client: add transfer_erc20 function

* spec: add erc20 tests

* docs: update readme

* spec: add more erc20 tests
  • Loading branch information
q9f authored Jan 2, 2023
1 parent 096d793 commit d41b865
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 5 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ A straightforward library to build, sign, and broadcast Ethereum transactions. I

What you get:
- [x] Secp256k1 Key-Pairs and Encrypted Ethereum Key-Stores (JSON)
- [x] EIP-20 Token Transfers (ERC20)
- [x] EIP-55 Checksummed Ethereum Addresses
- [x] EIP-137 Ethereum Domain Name Service (ENS)
- [x] EIP-155 Replay protection with Chain IDs (with presets)
Expand All @@ -37,7 +38,7 @@ What you get:
- [x] RLP-Encoder and Decoder (including sedes)
- [x] RPC-Client (IPC/HTTP) for Execution-Layer APIs
- [x] Solidity bindings (compile contracts from Ruby)
- [x] ~~Full~~ Some smart-contract support (deploy, transact, and call)
- [x] Full smart-contract support (deploy, transact, and call)

## Installation
Add this line to your application's Gemfile:
Expand Down
39 changes: 36 additions & 3 deletions lib/eth/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def transfer_and_wait(destination, amount, **kwargs)
# if no sender key is provided.
#
# **Note**, that many remote providers (e.g., Infura) do not provide
# any accounts. Provide a `sender_key` if you experience issues.
# any accounts. Provide a `sender_key:` if you experience issues.
#
# @overload transfer(destination, amount)
# @param destination [Eth::Address] the destination address.
Expand Down Expand Up @@ -183,6 +183,39 @@ def transfer(destination, amount, **kwargs)
end
end

# Transfers a token that implements the ERC20 `transfer()` interface.
#
# See {#transfer_erc20} for params and overloads.
#
# @return [Object] returns the result of the transaction.
def transfer_erc20_and_wait(erc20_contract, destination, amount, **kwargs)
transact_and_wait(erc20_contract, "transfer", destination, amount, **kwargs)
end

# Transfers a token that implements the ERC20 `transfer()` interface.
#
# **Note**, that many remote providers (e.g., Infura) do not provide
# any accounts. Provide a `sender_key:` if you experience issues.
#
# @overload transfer_erc20(erc20_contract, destination, amount)
# @param erc20_contract [Eth::Contract] the ERC20 contract to write to.
# @param destination [Eth::Address] the destination address.
# @param amount [Integer] the transfer amount (mind the `decimals()`).
# @overload transfer_erc20(erc20_contract, destination, amount, **kwargs)
# @param erc20_contract [Eth::Contract] the ERC20 contract to write to.
# @param destination [Eth::Address] the destination address.
# @param amount [Integer] the transfer amount (mind the `decimals()`).
# @param **sender_key [Eth::Key] the sender private key.
# @param **legacy [Boolean] enables legacy transactions (pre-EIP-1559).
# @param **gas_limit [Integer] optional gas limit override for deploying the contract.
# @param **nonce [Integer] optional specific nonce for transaction.
# @param **tx_value [Integer] optional transaction value field filling.
# @return [Object] returns the result of the transaction.
def transfer_erc20(erc20_contract, destination, amount, **kwargs)
destination = destination.to_s if destination.instance_of? Eth::Address
transact(erc20_contract, "transfer", destination, amount, **kwargs)
end

# Deploys a contract and waits for it to be mined. Uses
# `eth_coinbase` or external signer if no sender key is provided.
#
Expand All @@ -199,7 +232,7 @@ def deploy_and_wait(contract, *args, **kwargs)
# if no sender key is provided.
#
# **Note**, that many remote providers (e.g., Infura) do not provide
# any accounts. Provide a `sender_key` if you experience issues.
# any accounts. Provide a `sender_key:` if you experience issues.
#
# @overload deploy(contract)
# @param contract [Eth::Contract] contracts to deploy.
Expand Down Expand Up @@ -305,7 +338,7 @@ def call(contract, function, *args, **kwargs)
# contract read/write).
#
# **Note**, that many remote providers (e.g., Infura) do not provide
# any accounts. Provide a `sender_key` if you experience issues.
# any accounts. Provide a `sender_key:` if you experience issues.
#
# @overload transact(contract, function)
# @param contract [Eth::Contract] the subject contract to write to.
Expand Down
33 changes: 33 additions & 0 deletions spec/eth/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,39 @@
end
end

describe ".transfer_erc20 .transfer_erc20_and_wait" do
subject(:key) { Key.new }
subject(:erc20) { Eth::Contract.from_file(file: "spec/fixtures/contracts/erc20.sol") }

it "deploys and mints erc20 tokens" do
geth_dev_ipc.transfer_and_wait(key.address, Unit::ETHER)
geth_dev_ipc.deploy_and_wait(erc20, "FooBarBaz Token", "FOO")
expect(geth_dev_ipc.call(erc20, "name")).to eq "FooBarBaz Token"
expect(geth_dev_ipc.call(erc20, "symbol")).to eq "FOO"
expect(geth_dev_ipc.call(erc20, "decimals")).to eq 18
geth_dev_ipc.transact_and_wait(erc20, "mint", key.address.to_s, Unit::ETHER)
expect(geth_dev_ipc.call(erc20, "balanceOf", key.address.to_s)).to eq Unit::ETHER
expect(geth_dev_ipc.call(erc20, "totalSupply")).to eq Unit::ETHER
end

it "transfers erc20 tokens" do
geth_dev_ipc.transfer_and_wait(key.address, Unit::ETHER)
geth_dev_ipc.deploy_and_wait(erc20, "FooBarBaz Token", "FOO")
geth_dev_ipc.transact_and_wait(erc20, "mint", geth_dev_ipc.default_account.to_s, Unit::ETHER)
geth_dev_ipc.transfer_erc20_and_wait(erc20, key.address.to_s, 17)
expect(geth_dev_ipc.call(erc20, "balanceOf", key.address.to_s)).to eq 17
expect(geth_dev_ipc.call(erc20, "balanceOf", geth_dev_ipc.default_account.to_s)).to eq Unit::ETHER - 17
expect(geth_dev_ipc.call(erc20, "totalSupply")).to eq Unit::ETHER
tx = geth_dev_ipc.transact(erc20, "mint", key.address.to_s, Unit::ETHER, sender_key: key)
geth_dev_ipc.wait_for_tx(tx)
tf = geth_dev_ipc.transfer_erc20(erc20, geth_dev_ipc.default_account.to_s, 17, sender_key: key)
geth_dev_ipc.wait_for_tx(tf)
expect(geth_dev_ipc.call(erc20, "balanceOf", key.address.to_s)).to eq Unit::ETHER
expect(geth_dev_ipc.call(erc20, "balanceOf", geth_dev_ipc.default_account.to_s)).to eq Unit::ETHER
expect(geth_dev_ipc.call(erc20, "totalSupply")).to eq 2 * Unit::ETHER
end
end

describe ".is_valid_signature" do
subject(:key) { Key.new priv: "8387af3ab105157d8fcdefdb41ef12aaa876c5123e2c57c9640dcdd74157b3b4" }
subject(:contract) { Contract.from_file(file: "spec/fixtures/contracts/signer.sol", contract_index: 1) }
Expand Down
3 changes: 2 additions & 1 deletion spec/fixtures/contracts/deposit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
pragma solidity ^0.8;

// Do not use this contract in production! It's untested and
// modified for this specific test-suite!
// modified for this specific test-suite! It contains insecure
// functions!

// ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━
// ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓
Expand Down
143 changes: 143 additions & 0 deletions spec/fixtures/contracts/erc20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (v4.8.0 - simplified)

pragma solidity ^0.8;

// Do not use this contract in production! It's untested and
// modified for this specific test-suite! It contains insecure
// functions!

contract ERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);

constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}

function name() public view virtual returns (string memory) {
return _name;
}

function symbol() public view virtual returns (string memory) {
return _symbol;
}

function decimals() public view virtual returns (uint8) {
return 18;
}

function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}

function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}

function transfer(address to, uint256 amount) public virtual returns (bool) {
address owner = msg.sender;
_transfer(owner, to, amount);
return true;
}

function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public virtual returns (bool) {
address owner = msg.sender;
_approve(owner, spender, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) {
address spender = msg.sender;
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}

function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = msg.sender;
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}

function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = msg.sender;
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}

function mint(address account, uint256 amount) public virtual returns (bool) {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
unchecked {
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
return true;
}

function burn(address account, uint256 amount) public virtual returns (bool) {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
return true;
}

function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}

function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}

function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}

function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}

0 comments on commit d41b865

Please sign in to comment.