Hi @kypanz,
With ERC20, if we transfer
an amount of tokens to a contracts address, the contract has no way of knowing that this occurred.
Instead we first need to approve
an allowance for the recipient contract, then in a second transaction, a function we call in the contract can call transferFrom
.
The following example consists of:
- SimpleToken, an ERC20 token where all the tokens are pre-assigned to the creator of the contract.
- SimpleTokenRecipient, a contract that charges tokens to perform an action (
doStuff
)
- SimpleTokenRecipient.test.js, tests that check for performing an action when an allowance is approved, and for revert when attempting to perform an action when an allowance isn’t approved.
The test uses OpenZeppelin Test Environment and Test Helpers.
Let me know if you have any questions.
SimpleToken.sol
pragma solidity ^0.5.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";
/**
* @title SimpleToken
* @dev Very simple ERC20 Token example, where all tokens are pre-assigned to the creator.
* Note they can later distribute these tokens as they wish using `transfer` and other
* `ERC20` functions.
*/
contract SimpleToken is ERC20, ERC20Detailed {
/**
* @dev Constructor that gives msg.sender all of existing tokens.
*/
constructor () public ERC20Detailed("SimpleToken", "SIM", 18) {
_mint(msg.sender, 10000 * (10 ** uint256(decimals())));
}
}
SimpleTokenRecipient.sol
pragma solidity ^0.5.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title SimpleTokenRecipient
* @dev Very simple ERC20 Recipient
*/
contract SimpleTokenRecipient {
IERC20 private _token;
event DoneStuff(address from, address to, uint256 amount);
constructor (address token) public {
_token = IERC20(token);
}
function doStuff() external {
address from = msg.sender;
address to = address(this);
uint256 amount = 1e18;
_token.transferFrom(from, to, amount);
emit DoneStuff(from, to, amount);
}
}
SimpleTokenRecipient.test.js
const { accounts, contract } = require('@openzeppelin/test-environment');
const { expect } = require('chai');
const {
BN, // Big Number support
constants, // Common constants, like the zero address and largest integers
expectEvent, // Assertions for emitted events
expectRevert, // Assertions for transactions that should fail
} = require('@openzeppelin/test-helpers');
const SimpleToken = contract.fromArtifact('SimpleToken');
const SimpleTokenRecipient = contract.fromArtifact('SimpleTokenRecipient');
describe('SimpleTokenRecipient', function () {
const [ creator, holder ] = accounts;
beforeEach(async function () {
this.token = await SimpleToken.new({ from: creator });
this.recipient = await SimpleTokenRecipient.new(this.token.address);
this.tokenbits = new BN(10).pow(await this.token.decimals());
await this.token.transfer(holder, new BN(100).mul(this.tokenbits), { from: creator });
});
it('do stuff', async function () {
// token holder approves allowance
await this.token.increaseAllowance(this.recipient.address, new BN(1).mul(this.tokenbits), { from: holder });
// token holder gets recipient contract to do stuff
await this.recipient.doStuff({ from: holder });
// check balance of recipient contract
expect(await this.token.balanceOf(this.recipient.address))
.to.be.bignumber.equal(new BN(1).mul(this.tokenbits));
});
it('do stuff fails when allowance not approved', async function () {
// transaction reverts when attempting to do stuff without approving an allowance
await expectRevert(
this.recipient.doStuff({ from: holder }),
"ERC20: transfer amount exceeds allowance"
);
expect(await this.token.balanceOf(this.recipient.address))
.to.be.bignumber.equal(new BN(0));
});
});