Explorar el Código

Draft EIP 1820 (#1677)

* Add barebones EIP1820 support.

* Update openzeppelin-test-helpers dependency to have ERC1820 support.

* Add tests for ERC1820.

* Improve inline documentation.

* Add changelog entry.

* Update test-helpers, refactor tests to use new helpers.

* Rename ERC1820 to ERC1820Implementer.

* Improve implementer docstring.

* Remove _implementsInterfaceForAddress.

* update openzeppelin-test-helpers to 0.2.0

* Update contracts/drafts/ERC1820Implementer.sol

Co-Authored-By: nventuro <nicolas.venturo@gmail.com>

* Fix how solidity coverage is run to allow for free events.

* Fix coverage testing script.
Nicolás Venturo hace 6 años
padre
commit
308a4c9907

+ 5 - 0
CHANGELOG.md

@@ -1,5 +1,10 @@
 # Changelog
 
+## 2.3.0 (unreleased)
+
+### New features:
+ * `ERC1820`: added support for interacting with the [ERC1820](https://eips.ethereum.org/EIPS/eip-1820) registry contract (`IERC1820Registry`), as well as base contracts that can be registered as implementers there. ([#1677](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1677))
+
 ## 2.2.0 (2019-03-14)
 
 ### New features:

+ 24 - 0
contracts/drafts/ERC1820Implementer.sol

@@ -0,0 +1,24 @@
+pragma solidity ^0.5.2;
+
+import "./IERC1820Implementer.sol";
+
+/**
+ * @dev ERC1820Implementer allows your contract to implement an interface for another account in the sense of ERC1820.
+ * That account or one of its ERC1820 managers can register the implementer in the ERC1820 registry, but the registry
+ * will first check with the implementer if it agrees to it, and only allow it if it does. Using the internal
+ * function _registerInterfaceForAddress provided by this contract, you are expressing this agreement,
+ * and you will be able to register the contract as an implementer in the registry for that account.
+ */
+contract ERC1820Implementer is IERC1820Implementer {
+    bytes32 constant private ERC1820_ACCEPT_MAGIC = keccak256(abi.encodePacked("ERC1820_ACCEPT_MAGIC"));
+
+    mapping(bytes32 => mapping(address => bool)) private _supportedInterfaces;
+
+    function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view returns (bytes32) {
+        return _supportedInterfaces[interfaceHash][account] ? ERC1820_ACCEPT_MAGIC : bytes32(0x00);
+    }
+
+    function _registerInterfaceForAddress(bytes32 interfaceHash, address account) internal {
+        _supportedInterfaces[interfaceHash][account] = true;
+    }
+}

+ 17 - 0
contracts/drafts/IERC1820Implementer.sol

@@ -0,0 +1,17 @@
+pragma solidity ^0.5.2;
+
+/**
+ * @title IERC1820Implementer
+ * Interface for contracts that will be registered as implementers in the ERC1820 registry.
+ * @notice For more details, see https://eips.ethereum.org/EIPS/eip-1820
+ */
+interface IERC1820Implementer {
+    /**
+     * @notice Indicates whether the contract implements the interface `interfaceHash` for the address `account` or
+     * not.
+     * @param interfaceHash keccak256 hash of the name of the interface
+     * @param account Address for which the contract will implement the interface
+     * @return ERC1820_ACCEPT_MAGIC only if the contract implements `interfaceHash` for the address `account`.
+     */
+    function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view returns (bytes32);
+}

+ 90 - 0
contracts/drafts/IERC1820Registry.sol

@@ -0,0 +1,90 @@
+pragma solidity ^0.5.2;
+
+/**
+ * @title ERC1820 Pseudo-introspection Registry Contract
+ * @author Jordi Baylina and Jacques Dafflon
+ * @notice For more details, see https://eips.ethereum.org/EIPS/eip-1820
+ */
+interface IERC1820Registry {
+    /**
+     * @notice Sets the contract which implements a specific interface for an address.
+     * Only the manager defined for that address can set it.
+     * (Each address is the manager for itself until it sets a new manager.)
+     * @param account Address for which to set the interface.
+     * (If 'account' is the zero address then 'msg.sender' is assumed.)
+     * @param interfaceHash Keccak256 hash of the name of the interface as a string.
+     * E.g., 'web3.utils.keccak256("ERC777TokensRecipient")' for the 'ERC777TokensRecipient' interface.
+     * @param implementer Contract address implementing `interfaceHash` for `account.address()`.
+     */
+    function setInterfaceImplementer(address account, bytes32 interfaceHash, address implementer) external;
+
+    /**
+     * @notice Sets `newManager.address()` as manager for `account.address()`.
+     * The new manager will be able to call 'setInterfaceImplementer' for `account.address()`.
+     * @param account Address for which to set the new manager.
+     * @param newManager Address of the new manager for `addr.address()`.
+     * (Pass '0x0' to reset the manager to `account.address()`.)
+     */
+    function setManager(address account, address newManager) external;
+
+    /**
+     *  @notice Updates the cache with whether the contract implements an ERC165 interface or not.
+     *  @param account Address of the contract for which to update the cache.
+     *  @param interfaceId ERC165 interface for which to update the cache.
+     */
+    function updateERC165Cache(address account, bytes4 interfaceId) external;
+
+    /**
+     *  @notice Get the manager of an address.
+     *  @param account Address for which to return the manager.
+     *  @return Address of the manager for a given address.
+     */
+    function getManager(address account) external view returns (address);
+
+    /**
+     *  @notice Query if an address implements an interface and through which contract.
+     *  @param account Address being queried for the implementer of an interface.
+     *  (If 'account' is the zero address then 'msg.sender' is assumed.)
+     *  @param interfaceHash Keccak256 hash of the name of the interface as a string.
+     *  E.g., 'web3.utils.keccak256("ERC777TokensRecipient")' for the 'ERC777TokensRecipient' interface.
+     *  @return The address of the contract which implements the interface `interfaceHash` for `account.address()`
+     *  or '0' if `account.address()` did not register an implementer for this interface.
+     */
+    function getInterfaceImplementer(address account, bytes32 interfaceHash) external view returns (address);
+
+    /**
+     *  @notice Checks whether a contract implements an ERC165 interface or not.
+     *  If the result is not cached a direct lookup on the contract address is performed.
+     *  If the result is not cached or the cached value is out-of-date, the cache MUST be updated manually by calling
+     *  'updateERC165Cache' with the contract address.
+     *  @param account Address of the contract to check.
+     *  @param interfaceId ERC165 interface to check.
+     *  @return True if `account.address()` implements `interfaceId`, false otherwise.
+     */
+    function implementsERC165Interface(address account, bytes4 interfaceId) external view returns (bool);
+
+    /**
+     *  @notice Checks whether a contract implements an ERC165 interface or not without using nor updating the cache.
+     *  @param account Address of the contract to check.
+     *  @param interfaceId ERC165 interface to check.
+     *  @return True if `account.address()` implements `interfaceId`, false otherwise.
+     */
+    function implementsERC165InterfaceNoCache(address account, bytes4 interfaceId) external view returns (bool);
+
+    /**
+     *  @notice Compute the keccak256 hash of an interface given its name.
+     *  @param interfaceName Name of the interface.
+     *  @return The keccak256 hash of an interface name.
+     */
+    function interfaceHash(string calldata interfaceName) external pure returns (bytes32);
+
+    /**
+     *  @notice Indicates a contract is the `implementer` of `interfaceHash` for `account`.
+     */
+    event InterfaceImplementerSet(address indexed account, bytes32 indexed interfaceHash, address indexed implementer);
+
+    /**
+     *  @notice Indicates `newManager` is the address of the new manager for `account`.
+     */
+    event ManagerChanged(address indexed account, address indexed newManager);
+}

+ 9 - 0
contracts/mocks/ERC1820ImplementerMock.sol

@@ -0,0 +1,9 @@
+pragma solidity ^0.5.2;
+
+import "../drafts/ERC1820Implementer.sol";
+
+contract ERC1820ImplementerMock is ERC1820Implementer {
+    function registerInterfaceForAddress(bytes32 interfaceHash, address account) public {
+        _registerInterfaceForAddress(interfaceHash, account);
+    }
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 7836 - 1844
package-lock.json


+ 4 - 3
package.json

@@ -51,11 +51,12 @@
     "eslint-plugin-standard": "^3.1.0",
     "ethereumjs-util": "^6.0.0",
     "ethjs-abi": "^0.2.1",
-    "ganache-cli": "6.1.8",
-    "openzeppelin-test-helpers": "^0.1.1",
+    "ganache-cli": "^6.4.1",
+    "ganache-cli-coverage": "github:Agusx1211/ganache-cli#c462b3fc48fe9b16756f7799885c0741114d9ed3",
+    "openzeppelin-test-helpers": "^0.2.0",
     "pify": "^4.0.1",
     "solhint": "^1.5.0",
-    "solidity-coverage": "https://github.com:rotcivegaf/solidity-coverage.git#5875f5b7bc74d447f3312c9c0e9fc7814b482477",
+    "solidity-coverage": "github:rotcivegaf/solidity-coverage#5875f5b7bc74d447f3312c9c0e9fc7814b482477",
     "truffle": "^5.0.0"
   },
   "dependencies": {}

+ 1 - 1
scripts/test.sh

@@ -39,7 +39,7 @@ start_ganache() {
   )
 
   if [ "$SOLIDITY_COVERAGE" = true ]; then
-    node_modules/.bin/testrpc-sc --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null &
+    node_modules/.bin/ganache-cli-coverage --emitFreeLogs true --allowUnlimitedContractSize true --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null &
   else
     node_modules/.bin/ganache-cli --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null &
   fi

+ 3 - 3
test/crowdsale/AllowanceCrowdsale.test.js

@@ -47,9 +47,9 @@ contract('AllowanceCrowdsale', function ([_, investor, wallet, purchaser, tokenW
     });
 
     it('should forward funds to wallet', async function () {
-      (await balance.difference(wallet, () =>
-        this.crowdsale.sendTransaction({ value, from: investor }))
-      ).should.be.bignumber.equal(value);
+      const balanceTracker = await balance.tracker(wallet);
+      await this.crowdsale.sendTransaction({ value, from: investor });
+      (await balanceTracker.delta()).should.be.bignumber.equal(value);
     });
   });
 

+ 6 - 6
test/crowdsale/Crowdsale.test.js

@@ -88,9 +88,9 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
         });
 
         it('should forward funds to wallet', async function () {
-          (await balance.difference(wallet, () =>
-            this.crowdsale.sendTransaction({ value, from: investor }))
-          ).should.be.bignumber.equal(value);
+          const balanceTracker = await balance.tracker(wallet);
+          await this.crowdsale.sendTransaction({ value, from: investor });
+          (await balanceTracker.delta()).should.be.bignumber.equal(value);
         });
       });
 
@@ -111,9 +111,9 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
         });
 
         it('should forward funds to wallet', async function () {
-          (await balance.difference(wallet, () =>
-            this.crowdsale.buyTokens(investor, { value, from: purchaser }))
-          ).should.be.bignumber.equal(value);
+          const balanceTracker = await balance.tracker(wallet);
+          await this.crowdsale.buyTokens(investor, { value, from: purchaser });
+          (await balanceTracker.delta()).should.be.bignumber.equal(value);
         });
       });
     });

+ 3 - 3
test/crowdsale/MintedCrowdsale.behavior.js

@@ -28,9 +28,9 @@ function shouldBehaveLikeMintedCrowdsale ([_, investor, wallet, purchaser], rate
       });
 
       it('should forward funds to wallet', async function () {
-        (await balance.difference(wallet, () =>
-          this.crowdsale.sendTransaction({ value, from: investor }))
-        ).should.be.bignumber.equal(value);
+        const balanceTracker = await balance.tracker(wallet);
+        await this.crowdsale.sendTransaction({ value, from: investor });
+        (await balanceTracker.delta()).should.be.bignumber.equal(value);
       });
     });
   });

+ 3 - 3
test/crowdsale/RefundableCrowdsale.test.js

@@ -65,9 +65,9 @@ contract('RefundableCrowdsale', function ([_, wallet, investor, purchaser, anyon
           });
 
           it('refunds', async function () {
-            (await balance.difference(investor, () =>
-              this.crowdsale.claimRefund(investor, { gasPrice: 0 }))
-            ).should.be.bignumber.equal(lessThanGoal);
+            const balanceTracker = await balance.tracker(investor);
+            await this.crowdsale.claimRefund(investor, { gasPrice: 0 });
+            (await balanceTracker.delta()).should.be.bignumber.equal(lessThanGoal);
           });
         });
       });

+ 61 - 0
test/drafts/ERC1820Implementer.test.js

@@ -0,0 +1,61 @@
+const { shouldFail, singletons } = require('openzeppelin-test-helpers');
+const { bufferToHex, keccak256 } = require('ethereumjs-util');
+
+const ERC1820ImplementerMock = artifacts.require('ERC1820ImplementerMock');
+
+contract('ERC1820Implementer', function ([_, registryFunder, implementee, anyone]) {
+  const ERC1820_ACCEPT_MAGIC = bufferToHex(keccak256('ERC1820_ACCEPT_MAGIC'));
+
+  beforeEach(async function () {
+    this.implementer = await ERC1820ImplementerMock.new();
+    this.registry = await singletons.ERC1820Registry(registryFunder);
+
+    this.interfaceA = bufferToHex(keccak256('interfaceA'));
+    this.interfaceB = bufferToHex(keccak256('interfaceB'));
+  });
+
+  context('with no registered interfaces', function () {
+    it('returns false when interface implementation is queried', async function () {
+      (await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee))
+        .should.not.equal(ERC1820_ACCEPT_MAGIC);
+    });
+
+    it('reverts when attempting to set as implementer in the registry', async function () {
+      await shouldFail.reverting(
+        this.registry.setInterfaceImplementer(
+          implementee, this.interfaceA, this.implementer.address, { from: implementee }
+        )
+      );
+    });
+  });
+
+  context('with registered interfaces', function () {
+    beforeEach(async function () {
+      await this.implementer.registerInterfaceForAddress(this.interfaceA, implementee);
+    });
+
+    it('returns true when interface implementation is queried', async function () {
+      (await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee))
+        .should.equal(ERC1820_ACCEPT_MAGIC);
+    });
+
+    it('returns false when interface implementation for non-supported interfaces is queried', async function () {
+      (await this.implementer.canImplementInterfaceForAddress(this.interfaceB, implementee))
+        .should.not.equal(ERC1820_ACCEPT_MAGIC);
+    });
+
+    it('returns false when interface implementation for non-supported addresses is queried', async function () {
+      (await this.implementer.canImplementInterfaceForAddress(this.interfaceA, anyone))
+        .should.not.equal(ERC1820_ACCEPT_MAGIC);
+    });
+
+    it('can be set as an implementer for supported interfaces in the registry', async function () {
+      await this.registry.setInterfaceImplementer(
+        implementee, this.interfaceA, this.implementer.address, { from: implementee }
+      );
+
+      (await this.registry.getInterfaceImplementer(implementee, this.interfaceA))
+        .should.equal(this.implementer.address);
+    });
+  });
+});

+ 14 - 12
test/examples/SampleCrowdsale.test.js

@@ -72,21 +72,23 @@ contract('SampleCrowdsale', function ([_, deployer, owner, wallet, investor]) {
     await time.increaseTo(this.openingTime);
     await this.crowdsale.send(GOAL);
 
-    (await balance.difference(wallet, async () => {
-      await time.increaseTo(this.afterClosingTime);
-      await this.crowdsale.finalize({ from: owner });
-    })).should.be.bignumber.equal(GOAL);
+    const balanceTracker = await balance.tracker(wallet);
+    await time.increaseTo(this.afterClosingTime);
+    await this.crowdsale.finalize({ from: owner });
+    (await balanceTracker.delta()).should.be.bignumber.equal(GOAL);
   });
 
   it('should allow refunds if the goal is not reached', async function () {
-    (await balance.difference(investor, async () => {
-      await time.increaseTo(this.openingTime);
-      await this.crowdsale.sendTransaction({ value: ether('1'), from: investor, gasPrice: 0 });
-      await time.increaseTo(this.afterClosingTime);
-
-      await this.crowdsale.finalize({ from: owner });
-      await this.crowdsale.claimRefund(investor, { gasPrice: 0 });
-    })).should.be.bignumber.equal('0');
+    const balanceTracker = await balance.tracker(investor);
+
+    await time.increaseTo(this.openingTime);
+    await this.crowdsale.sendTransaction({ value: ether('1'), from: investor, gasPrice: 0 });
+    await time.increaseTo(this.afterClosingTime);
+
+    await this.crowdsale.finalize({ from: owner });
+    await this.crowdsale.claimRefund(investor, { gasPrice: 0 });
+
+    (await balanceTracker.delta()).should.be.bignumber.equal('0');
   });
 
   describe('when goal > cap', function () {

+ 6 - 5
test/payment/PullPayment.test.js

@@ -30,13 +30,14 @@ contract('PullPayment', function ([_, payer, payee1, payee2]) {
   });
 
   it('can withdraw payment', async function () {
-    (await balance.difference(payee1, async () => {
-      await this.contract.callTransfer(payee1, amount, { from: payer });
-      (await this.contract.payments(payee1)).should.be.bignumber.equal(amount);
+    const balanceTracker = await balance.tracker(payee1);
 
-      await this.contract.withdrawPayments(payee1);
-    })).should.be.bignumber.equal(amount);
+    await this.contract.callTransfer(payee1, amount, { from: payer });
+    (await this.contract.payments(payee1)).should.be.bignumber.equal(amount);
 
+    await this.contract.withdrawPayments(payee1);
+
+    (await balanceTracker.delta()).should.be.bignumber.equal(amount);
     (await this.contract.payments(payee1)).should.be.bignumber.equal('0');
   });
 });

+ 6 - 4
test/payment/escrow/Escrow.behavior.js

@@ -52,10 +52,12 @@ function shouldBehaveLikeEscrow (primary, [payee1, payee2]) {
 
     describe('withdrawals', async function () {
       it('can withdraw payments', async function () {
-        (await balance.difference(payee1, async () => {
-          await this.escrow.deposit(payee1, { from: primary, value: amount });
-          await this.escrow.withdraw(payee1, { from: primary });
-        })).should.be.bignumber.equal(amount);
+        const balanceTracker = await balance.tracker(payee1);
+
+        await this.escrow.deposit(payee1, { from: primary, value: amount });
+        await this.escrow.withdraw(payee1, { from: primary });
+
+        (await balanceTracker.delta()).should.be.bignumber.equal(amount);
 
         (await balance.current(this.escrow.address)).should.be.bignumber.equal('0');
         (await this.escrow.depositsOf(payee1)).should.be.bignumber.equal('0');

+ 6 - 6
test/payment/escrow/RefundEscrow.test.js

@@ -64,9 +64,9 @@ contract('RefundEscrow', function ([_, primary, beneficiary, refundee1, refundee
       });
 
       it('allows beneficiary withdrawal', async function () {
-        (await balance.difference(beneficiary, () =>
-          this.escrow.beneficiaryWithdraw()
-        )).should.be.bignumber.equal(amount.muln(refundees.length));
+        const balanceTracker = await balance.tracker(beneficiary);
+        await this.escrow.beneficiaryWithdraw();
+        (await balanceTracker.delta()).should.be.bignumber.equal(amount.muln(refundees.length));
       });
 
       it('prevents entering the refund state', async function () {
@@ -98,9 +98,9 @@ contract('RefundEscrow', function ([_, primary, beneficiary, refundee1, refundee
 
       it('refunds refundees', async function () {
         for (const refundee of [refundee1, refundee2]) {
-          (await balance.difference(refundee, () =>
-            this.escrow.withdraw(refundee, { from: primary }))
-          ).should.be.bignumber.equal(amount);
+          const balanceTracker = await balance.tracker(refundee);
+          await this.escrow.withdraw(refundee, { from: primary });
+          (await balanceTracker.delta()).should.be.bignumber.equal(amount);
         }
       });
 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio