Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer replacement #1962

Merged
merged 4 commits into from
Oct 25, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### New features:
* `Address.toPayable`: added a helper to convert between address types without having to resort to low-level casting. ([#1773](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1773))
* Facilities to make metatransaction-enabled contracts through the Gas Station Network. ([#1844](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1844))
* `Address.sendEther`: added a replacement to Solidity's `transfer`, removing the fixed gas stipend. ([#1961](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1961))
nventuro marked this conversation as resolved.
Show resolved Hide resolved

### Improvements:
* `Address.isContract`: switched from `extcodesize` to `extcodehash` for less gas usage. ([#1802](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1802))
Expand Down
6 changes: 6 additions & 0 deletions contracts/mocks/AddressImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ contract AddressImpl {
function toPayable(address account) external pure returns (address payable) {
return Address.toPayable(account);
}

function sendEther(address payable receiver, uint256 amount) external {
Address.sendEther(receiver, amount);
}

function () external payable { } // sendEther's tests require the contract to hold Ether
}
15 changes: 15 additions & 0 deletions contracts/mocks/EtherReceiverMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pragma solidity ^0.5.0;

contract EtherReceiverMock {
bool private _acceptEther;

function setAcceptEther(bool acceptEther) public {
_acceptEther = acceptEther;
}

function () external payable {
if (!_acceptEther) {
revert();
}
}
}
24 changes: 24 additions & 0 deletions contracts/utils/Address.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,28 @@ library Address {
function toPayable(address account) internal pure returns (address payable) {
return address(uint160(account));
}

/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendEther} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendEther(address payable recipient, uint256 amount) internal {
nventuro marked this conversation as resolved.
Show resolved Hide resolved
require(address(this).balance >= amount, "Address: not enough ether to send");

// solhint-disable-next-line avoid-call-value
(bool success, ) = recipient.call.value(amount)("");
require(success, "Address: unable to send ether");
}
}
71 changes: 69 additions & 2 deletions test/utils/Address.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const { constants } = require('@openzeppelin/test-helpers');
const { balance, constants, ether, expectRevert, send } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');

const AddressImpl = artifacts.require('AddressImpl');
const SimpleToken = artifacts.require('SimpleToken');
const EtherReceiver = artifacts.require('EtherReceiverMock');

contract('Address', function ([_, other]) {
contract('Address', function ([_, recipient, other]) {
const ALL_ONES_ADDRESS = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF';

beforeEach(async function () {
Expand Down Expand Up @@ -35,4 +36,70 @@ contract('Address', function ([_, other]) {
expect(await this.mock.toPayable(ALL_ONES_ADDRESS)).to.equal(ALL_ONES_ADDRESS);
});
});

describe('sendEther', function () {
beforeEach(async function () {
this.recipientTracker = await balance.tracker(recipient);
});

context('when sender contract has no funds', function () {
it('sends 0 wei', async function () {
await this.mock.sendEther(other, 0);

expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0');
});

it('reverts when sending non-zero amounts', async function () {
await expectRevert(this.mock.sendEther(other, 1), 'Address: not enough ether to send');
});
});

context('when sender contract has funds', function () {
const funds = ether('1');
beforeEach(async function () {
await send.ether(other, this.mock.address, funds);
nventuro marked this conversation as resolved.
Show resolved Hide resolved
});

it('sends 0 wei', async function () {
await this.mock.sendEther(recipient, 0);
expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0');
});

it('sends non-zero amounts', async function () {
await this.mock.sendEther(recipient, funds.subn(1));
expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds.subn(1));
});

it('sends the whole balance', async function () {
await this.mock.sendEther(recipient, funds);
expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds);
expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0');
});

it('reverts when sending more than the balance', async function () {
await expectRevert(this.mock.sendEther(recipient, funds.addn(1)), 'Address: not enough ether to send');
});

context('with contract recipient', function () {
beforeEach(async function () {
this.contractRecipient = await EtherReceiver.new();
});

it('sends funds', async function () {
const tracker = await balance.tracker(this.contractRecipient.address);

await this.contractRecipient.setAcceptEther(true);
await this.mock.sendEther(this.contractRecipient.address, funds);
expect(await tracker.delta()).to.be.bignumber.equal(funds);
});

it('reverts on recipient revert', async function () {
await this.contractRecipient.setAcceptEther(false);
await expectRevert(
this.mock.sendEther(this.contractRecipient.address, funds), 'Address: unable to send ether'
);
});
});
});
});
});