Skip to content

Commit 3b4c951

Browse files
authored
Fix ERC777 potential reentrancy issues (#2483)
1 parent c2c08af commit 3b4c951

File tree

5 files changed

+50
-2
lines changed

5 files changed

+50
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
* `EnumerableMap`: fix a memory allocation issue by adding new `EnumerableMap.tryGet(uint)→(bool,address)` functions. `EnumerableMap.get(uint)→string` is now deprecated. ([#2462](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2462))
1616
* `ERC165Checker`: added batch `getSupportedInterfaces`. ([#2469](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2469))
1717

18+
### Security Fixes
19+
20+
* `ERC777`: fix potential reentrancy issues for custom extensions to `ERC777`. ([#2483](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2483))
21+
22+
If you're using our implementation of ERC777 from version 3.3.0 or earlier, and you define a custom `_beforeTokenTransfer` function that writes to a storage variable, you may be vulnerable to a reentrancy attack. If you're affected and would like assistance please write to [email protected]. [Read more in the pull request.](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2483)
23+
1824
## 3.3.0 (2020-11-26)
1925

2026
* Now supports both Solidity 0.6 and 0.7. Compiling with solc 0.7 will result in warnings. Install the `solc-0.7` tag to compile without warnings.

contracts/mocks/ERC777Mock.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import "../utils/Context.sol";
66
import "../token/ERC777/ERC777.sol";
77

88
contract ERC777Mock is Context, ERC777 {
9+
event BeforeTokenTransfer();
10+
911
constructor(
1012
address initialHolder,
1113
uint256 initialBalance,
@@ -28,4 +30,8 @@ contract ERC777Mock is Context, ERC777 {
2830
function approveInternal(address holder, address spender, uint256 value) public {
2931
_approve(holder, spender, value);
3032
}
33+
34+
function _beforeTokenTransfer(address, address, address, uint256) internal override {
35+
emit BeforeTokenTransfer();
36+
}
3137
}

contracts/mocks/ERC777SenderRecipientMock.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ contract ERC777SenderRecipientMock is Context, IERC777Sender, IERC777Recipient,
3434
uint256 toBalance
3535
);
3636

37+
// Emitted in ERC777Mock. Here for easier decoding
38+
event BeforeTokenTransfer();
39+
3740
bool private _shouldRevertSend;
3841
bool private _shouldRevertReceive;
3942

contracts/token/ERC777/ERC777.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,10 +390,10 @@ contract ERC777 is Context, IERC777, IERC20 {
390390

391391
address operator = _msgSender();
392392

393-
_beforeTokenTransfer(operator, from, address(0), amount);
394-
395393
_callTokensToSend(operator, from, address(0), amount, data, operatorData);
396394

395+
_beforeTokenTransfer(operator, from, address(0), amount);
396+
397397
// Update state variables
398398
_balances[from] = _balances[from].sub(amount, "ERC777: burn amount exceeds balance");
399399
_totalSupply = _totalSupply.sub(amount);

test/token/ERC777/ERC777.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,4 +447,37 @@ contract('ERC777', function (accounts) {
447447
expect(await this.token.defaultOperators()).to.deep.equal([]);
448448
});
449449
});
450+
451+
describe('relative order of hooks', function () {
452+
beforeEach(async function () {
453+
await singletons.ERC1820Registry(registryFunder);
454+
this.sender = await ERC777SenderRecipientMock.new();
455+
await this.sender.registerRecipient(this.sender.address);
456+
await this.sender.registerSender(this.sender.address);
457+
this.token = await ERC777.new(holder, initialSupply, name, symbol, []);
458+
await this.token.send(this.sender.address, 1, '0x', { from: holder });
459+
});
460+
461+
it('send', async function () {
462+
const { receipt } = await this.sender.send(this.token.address, anyone, 1, '0x');
463+
464+
const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
465+
expect(internalBeforeHook).to.be.gte(0);
466+
const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
467+
expect(externalSendHook).to.be.gte(0);
468+
469+
expect(externalSendHook).to.be.lt(internalBeforeHook);
470+
});
471+
472+
it('burn', async function () {
473+
const { receipt } = await this.sender.burn(this.token.address, 1, '0x');
474+
475+
const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
476+
expect(internalBeforeHook).to.be.gte(0);
477+
const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
478+
expect(externalSendHook).to.be.gte(0);
479+
480+
expect(externalSendHook).to.be.lt(internalBeforeHook);
481+
});
482+
});
450483
});

0 commit comments

Comments
 (0)