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 all 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.sendValue`: added a replacement to Solidity's `transfer`, removing the fixed gas stipend. ([#1961](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1961))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nventuro should it be 1962?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good catch. I'll fix it on master.


### 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 sendValue(address payable receiver, uint256 amount) external {
Address.sendValue(receiver, amount);
}

function () external payable { } // sendValue'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`. {sendValue} 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 sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");

// solhint-disable-next-line avoid-call-value
(bool success, ) = recipient.call.value(amount)("");
require(success, "Address: unable to send value, recipient may have reverted");
}
}
72 changes: 70 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,71 @@ contract('Address', function ([_, other]) {
expect(await this.mock.toPayable(ALL_ONES_ADDRESS)).to.equal(ALL_ONES_ADDRESS);
});
});

describe('sendValue', 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.sendValue(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.sendValue(other, 1), 'Address: insufficient balance');
});
});

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.sendValue(recipient, 0);
expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0');
});

it('sends non-zero amounts', async function () {
await this.mock.sendValue(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.sendValue(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.sendValue(recipient, funds.addn(1)), 'Address: insufficient balance');
});

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.sendValue(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.sendValue(this.contractRecipient.address, funds),
'Address: unable to send value, recipient may have reverted'
);
});
});
});
});
});