ERC777.test.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. const { BN, constants, expectEvent, expectRevert, singletons } = require('@openzeppelin/test-helpers');
  2. const { ZERO_ADDRESS } = constants;
  3. const { expect } = require('chai');
  4. const {
  5. shouldBehaveLikeERC777DirectSendBurn,
  6. shouldBehaveLikeERC777OperatorSendBurn,
  7. shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
  8. shouldBehaveLikeERC777InternalMint,
  9. shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
  10. shouldBehaveLikeERC777SendBurnWithSendHook,
  11. } = require('./ERC777.behavior');
  12. const { shouldBehaveLikeERC20, shouldBehaveLikeERC20Approve } = require('../ERC20/ERC20.behavior');
  13. const ERC777 = artifacts.require('$ERC777Mock');
  14. const ERC777SenderRecipientMock = artifacts.require('$ERC777SenderRecipientMock');
  15. contract('ERC777', function (accounts) {
  16. const [registryFunder, holder, defaultOperatorA, defaultOperatorB, newOperator, anyone] = accounts;
  17. const initialSupply = new BN('10000');
  18. const name = 'ERC777Test';
  19. const symbol = '777T';
  20. const data = web3.utils.sha3('OZ777TestData');
  21. const operatorData = web3.utils.sha3('OZ777TestOperatorData');
  22. const defaultOperators = [defaultOperatorA, defaultOperatorB];
  23. beforeEach(async function () {
  24. this.erc1820 = await singletons.ERC1820Registry(registryFunder);
  25. });
  26. context('with default operators', function () {
  27. beforeEach(async function () {
  28. this.token = await ERC777.new(name, symbol, defaultOperators);
  29. await this.token.$_mint(holder, initialSupply, '0x', '0x');
  30. });
  31. describe('as an ERC20 token', function () {
  32. shouldBehaveLikeERC20('ERC777', initialSupply, holder, anyone, defaultOperatorA);
  33. describe('_approve', function () {
  34. shouldBehaveLikeERC20Approve('ERC777', holder, anyone, initialSupply, function (owner, spender, amount) {
  35. return this.token.$_approve(owner, spender, amount);
  36. });
  37. describe('when the owner is the zero address', function () {
  38. it('reverts', async function () {
  39. await expectRevert(
  40. this.token.$_approve(ZERO_ADDRESS, anyone, initialSupply),
  41. 'ERC777: approve from the zero address',
  42. );
  43. });
  44. });
  45. });
  46. });
  47. it('does not emit AuthorizedOperator events for default operators', async function () {
  48. await expectEvent.notEmitted.inConstruction(this.token, 'AuthorizedOperator');
  49. });
  50. describe('basic information', function () {
  51. it('returns the name', async function () {
  52. expect(await this.token.name()).to.equal(name);
  53. });
  54. it('returns the symbol', async function () {
  55. expect(await this.token.symbol()).to.equal(symbol);
  56. });
  57. it('returns a granularity of 1', async function () {
  58. expect(await this.token.granularity()).to.be.bignumber.equal('1');
  59. });
  60. it('returns the default operators', async function () {
  61. expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
  62. });
  63. it('default operators are operators for all accounts', async function () {
  64. for (const operator of defaultOperators) {
  65. expect(await this.token.isOperatorFor(operator, anyone)).to.equal(true);
  66. }
  67. });
  68. it('returns the total supply', async function () {
  69. expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
  70. });
  71. it('returns 18 when decimals is called', async function () {
  72. expect(await this.token.decimals()).to.be.bignumber.equal('18');
  73. });
  74. it('the ERC777Token interface is registered in the registry', async function () {
  75. expect(
  76. await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC777Token')),
  77. ).to.equal(this.token.address);
  78. });
  79. it('the ERC20Token interface is registered in the registry', async function () {
  80. expect(
  81. await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC20Token')),
  82. ).to.equal(this.token.address);
  83. });
  84. });
  85. describe('balanceOf', function () {
  86. context('for an account with no tokens', function () {
  87. it('returns zero', async function () {
  88. expect(await this.token.balanceOf(anyone)).to.be.bignumber.equal('0');
  89. });
  90. });
  91. context('for an account with tokens', function () {
  92. it('returns their balance', async function () {
  93. expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply);
  94. });
  95. });
  96. });
  97. context('with no ERC777TokensSender and no ERC777TokensRecipient implementers', function () {
  98. describe('send/burn', function () {
  99. shouldBehaveLikeERC777DirectSendBurn(holder, anyone, data);
  100. context('with self operator', function () {
  101. shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, holder, data, operatorData);
  102. });
  103. context('with first default operator', function () {
  104. shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorA, data, operatorData);
  105. });
  106. context('with second default operator', function () {
  107. shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorB, data, operatorData);
  108. });
  109. context('before authorizing a new operator', function () {
  110. shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
  111. });
  112. context('with new authorized operator', function () {
  113. beforeEach(async function () {
  114. await this.token.authorizeOperator(newOperator, { from: holder });
  115. });
  116. shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, newOperator, data, operatorData);
  117. context('with revoked operator', function () {
  118. beforeEach(async function () {
  119. await this.token.revokeOperator(newOperator, { from: holder });
  120. });
  121. shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
  122. });
  123. });
  124. });
  125. describe('mint (internal)', function () {
  126. const to = anyone;
  127. const amount = new BN('5');
  128. context('with default operator', function () {
  129. const operator = defaultOperatorA;
  130. shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
  131. });
  132. context('with non operator', function () {
  133. const operator = newOperator;
  134. shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
  135. });
  136. });
  137. describe('mint (internal extended)', function () {
  138. const amount = new BN('5');
  139. context('to anyone', function () {
  140. beforeEach(async function () {
  141. this.recipient = anyone;
  142. });
  143. context('with default operator', function () {
  144. const operator = defaultOperatorA;
  145. it('without requireReceptionAck', async function () {
  146. await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
  147. });
  148. it('with requireReceptionAck', async function () {
  149. await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator });
  150. });
  151. });
  152. context('with non operator', function () {
  153. const operator = newOperator;
  154. it('without requireReceptionAck', async function () {
  155. await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
  156. });
  157. it('with requireReceptionAck', async function () {
  158. await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator });
  159. });
  160. });
  161. });
  162. context('to non ERC777TokensRecipient implementer', function () {
  163. beforeEach(async function () {
  164. this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
  165. this.recipient = this.tokensRecipientImplementer.address;
  166. });
  167. context('with default operator', function () {
  168. const operator = defaultOperatorA;
  169. it('without requireReceptionAck', async function () {
  170. await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
  171. });
  172. it('with requireReceptionAck', async function () {
  173. await expectRevert(
  174. this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
  175. 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
  176. );
  177. });
  178. });
  179. context('with non operator', function () {
  180. const operator = newOperator;
  181. it('without requireReceptionAck', async function () {
  182. await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
  183. });
  184. it('with requireReceptionAck', async function () {
  185. await expectRevert(
  186. this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
  187. 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
  188. );
  189. });
  190. });
  191. });
  192. });
  193. });
  194. describe('operator management', function () {
  195. it('accounts are their own operator', async function () {
  196. expect(await this.token.isOperatorFor(holder, holder)).to.equal(true);
  197. });
  198. it('reverts when self-authorizing', async function () {
  199. await expectRevert(
  200. this.token.authorizeOperator(holder, { from: holder }),
  201. 'ERC777: authorizing self as operator',
  202. );
  203. });
  204. it('reverts when self-revoking', async function () {
  205. await expectRevert(this.token.revokeOperator(holder, { from: holder }), 'ERC777: revoking self as operator');
  206. });
  207. it('non-operators can be revoked', async function () {
  208. expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
  209. const receipt = await this.token.revokeOperator(newOperator, { from: holder });
  210. expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
  211. expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
  212. });
  213. it('non-operators can be authorized', async function () {
  214. expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
  215. const receipt = await this.token.authorizeOperator(newOperator, { from: holder });
  216. expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
  217. expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true);
  218. });
  219. describe('new operators', function () {
  220. beforeEach(async function () {
  221. await this.token.authorizeOperator(newOperator, { from: holder });
  222. });
  223. it('are not added to the default operators list', async function () {
  224. expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
  225. });
  226. it('can be re-authorized', async function () {
  227. const receipt = await this.token.authorizeOperator(newOperator, { from: holder });
  228. expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
  229. expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true);
  230. });
  231. it('can be revoked', async function () {
  232. const receipt = await this.token.revokeOperator(newOperator, { from: holder });
  233. expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
  234. expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
  235. });
  236. });
  237. describe('default operators', function () {
  238. it('can be re-authorized', async function () {
  239. const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
  240. expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
  241. expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true);
  242. });
  243. it('can be revoked', async function () {
  244. const receipt = await this.token.revokeOperator(defaultOperatorA, { from: holder });
  245. expectEvent(receipt, 'RevokedOperator', { operator: defaultOperatorA, tokenHolder: holder });
  246. expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(false);
  247. });
  248. it('cannot be revoked for themselves', async function () {
  249. await expectRevert(
  250. this.token.revokeOperator(defaultOperatorA, { from: defaultOperatorA }),
  251. 'ERC777: revoking self as operator',
  252. );
  253. });
  254. context('with revoked default operator', function () {
  255. beforeEach(async function () {
  256. await this.token.revokeOperator(defaultOperatorA, { from: holder });
  257. });
  258. it('default operator is not revoked for other holders', async function () {
  259. expect(await this.token.isOperatorFor(defaultOperatorA, anyone)).to.equal(true);
  260. });
  261. it('other default operators are not revoked', async function () {
  262. expect(await this.token.isOperatorFor(defaultOperatorB, holder)).to.equal(true);
  263. });
  264. it('default operators list is not modified', async function () {
  265. expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
  266. });
  267. it('revoked default operator can be re-authorized', async function () {
  268. const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
  269. expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
  270. expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true);
  271. });
  272. });
  273. });
  274. });
  275. describe('send and receive hooks', function () {
  276. const amount = new BN('1');
  277. const operator = defaultOperatorA;
  278. // sender and recipient are stored inside 'this', since in some tests their addresses are determined dynamically
  279. describe('tokensReceived', function () {
  280. beforeEach(function () {
  281. this.sender = holder;
  282. });
  283. context('with no ERC777TokensRecipient implementer', function () {
  284. context('with contract recipient', function () {
  285. beforeEach(async function () {
  286. this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
  287. this.recipient = this.tokensRecipientImplementer.address;
  288. // Note that tokensRecipientImplementer doesn't implement the recipient interface for the recipient
  289. });
  290. it('send reverts', async function () {
  291. await expectRevert(
  292. this.token.send(this.recipient, amount, data, { from: holder }),
  293. 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
  294. );
  295. });
  296. it('operatorSend reverts', async function () {
  297. await expectRevert(
  298. this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }),
  299. 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
  300. );
  301. });
  302. it('mint (internal) reverts', async function () {
  303. await expectRevert(
  304. this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
  305. 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
  306. );
  307. });
  308. it('(ERC20) transfer succeeds', async function () {
  309. await this.token.transfer(this.recipient, amount, { from: holder });
  310. });
  311. it('(ERC20) transferFrom succeeds', async function () {
  312. const approved = anyone;
  313. await this.token.approve(approved, amount, { from: this.sender });
  314. await this.token.transferFrom(this.sender, this.recipient, amount, { from: approved });
  315. });
  316. });
  317. });
  318. context('with ERC777TokensRecipient implementer', function () {
  319. context('with contract as implementer for an externally owned account', function () {
  320. beforeEach(async function () {
  321. this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
  322. this.recipient = anyone;
  323. await this.tokensRecipientImplementer.recipientFor(this.recipient);
  324. await this.erc1820.setInterfaceImplementer(
  325. this.recipient,
  326. web3.utils.soliditySha3('ERC777TokensRecipient'),
  327. this.tokensRecipientImplementer.address,
  328. { from: this.recipient },
  329. );
  330. });
  331. shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
  332. });
  333. context('with contract as implementer for another contract', function () {
  334. beforeEach(async function () {
  335. this.recipientContract = await ERC777SenderRecipientMock.new();
  336. this.recipient = this.recipientContract.address;
  337. this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
  338. await this.tokensRecipientImplementer.recipientFor(this.recipient);
  339. await this.recipientContract.registerRecipient(this.tokensRecipientImplementer.address);
  340. });
  341. shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
  342. });
  343. context('with contract as implementer for itself', function () {
  344. beforeEach(async function () {
  345. this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
  346. this.recipient = this.tokensRecipientImplementer.address;
  347. await this.tokensRecipientImplementer.recipientFor(this.recipient);
  348. });
  349. shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
  350. });
  351. });
  352. });
  353. describe('tokensToSend', function () {
  354. beforeEach(function () {
  355. this.recipient = anyone;
  356. });
  357. context('with a contract as implementer for an externally owned account', function () {
  358. beforeEach(async function () {
  359. this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
  360. this.sender = holder;
  361. await this.tokensSenderImplementer.senderFor(this.sender);
  362. await this.erc1820.setInterfaceImplementer(
  363. this.sender,
  364. web3.utils.soliditySha3('ERC777TokensSender'),
  365. this.tokensSenderImplementer.address,
  366. { from: this.sender },
  367. );
  368. });
  369. shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
  370. });
  371. context('with contract as implementer for another contract', function () {
  372. beforeEach(async function () {
  373. this.senderContract = await ERC777SenderRecipientMock.new();
  374. this.sender = this.senderContract.address;
  375. this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
  376. await this.tokensSenderImplementer.senderFor(this.sender);
  377. await this.senderContract.registerSender(this.tokensSenderImplementer.address);
  378. // For the contract to be able to receive tokens (that it can later send), it must also implement the
  379. // recipient interface.
  380. await this.senderContract.recipientFor(this.sender);
  381. await this.token.send(this.sender, amount, data, { from: holder });
  382. });
  383. shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
  384. });
  385. context('with a contract as implementer for itself', function () {
  386. beforeEach(async function () {
  387. this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
  388. this.sender = this.tokensSenderImplementer.address;
  389. await this.tokensSenderImplementer.senderFor(this.sender);
  390. // For the contract to be able to receive tokens (that it can later send), it must also implement the
  391. // recipient interface.
  392. await this.tokensSenderImplementer.recipientFor(this.sender);
  393. await this.token.send(this.sender, amount, data, { from: holder });
  394. });
  395. shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
  396. });
  397. });
  398. });
  399. });
  400. context('with no default operators', function () {
  401. beforeEach(async function () {
  402. this.token = await ERC777.new(name, symbol, []);
  403. });
  404. it('default operators list is empty', async function () {
  405. expect(await this.token.defaultOperators()).to.deep.equal([]);
  406. });
  407. });
  408. describe('relative order of hooks', function () {
  409. beforeEach(async function () {
  410. await singletons.ERC1820Registry(registryFunder);
  411. this.sender = await ERC777SenderRecipientMock.new();
  412. await this.sender.registerRecipient(this.sender.address);
  413. await this.sender.registerSender(this.sender.address);
  414. this.token = await ERC777.new(name, symbol, []);
  415. await this.token.$_mint(this.sender.address, 1, '0x', '0x');
  416. });
  417. it('send', async function () {
  418. const { receipt } = await this.sender.send(this.token.address, anyone, 1, '0x');
  419. const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
  420. expect(internalBeforeHook).to.be.gte(0);
  421. const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
  422. expect(externalSendHook).to.be.gte(0);
  423. expect(externalSendHook).to.be.lt(internalBeforeHook);
  424. });
  425. it('burn', async function () {
  426. const { receipt } = await this.sender.burn(this.token.address, 1, '0x');
  427. const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
  428. expect(internalBeforeHook).to.be.gte(0);
  429. const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
  430. expect(externalSendHook).to.be.gte(0);
  431. expect(externalSendHook).to.be.lt(internalBeforeHook);
  432. });
  433. });
  434. });