diff --git a/README.md b/README.md index edf4299..d9bf97d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ERC20Pods -[![Build Status](https://github.com/1inch/erc20-pods/workflows/CI/badge.svg)](https://github.com/1inch/erc20-pods/actions) -[![Coverage Status](https://codecov.io/gh/1inch/erc20-pods/branch/master/graph/badge.svg?token=Z3D5O3XUYV)](https://codecov.io/gh/1inch/erc20-pods) -[![NPM Package](https://img.shields.io/npm/v/@1inch/erc20-pods.svg)](https://www.npmjs.org/package/@1inch/erc20-pods) +[![Build Status](https://github.com/1inch/token-pods/workflows/CI/badge.svg)](https://github.com/1inch/token-pods/actions) +[![Coverage Status](https://codecov.io/gh/1inch/token-pods/branch/master/graph/badge.svg?token=Z3D5O3XUYV)](https://codecov.io/gh/1inch/token-pods) +[![NPM Package](https://img.shields.io/npm/v/@1inch/token-pods.svg)](https://www.npmjs.org/package/@1inch/token-pods) ERC20 extension enabling external smart contract based Pods to track balances of those users who opted-in to these Pods. diff --git a/contracts/ERC1155Pods.sol b/contracts/ERC1155Pods.sol new file mode 100644 index 0000000..e031f19 --- /dev/null +++ b/contracts/ERC1155Pods.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +import "./interfaces/IERC1155Pods.sol"; +import "./TokenPodsLib.sol"; +import "./libs/ReentrancyGuard.sol"; + +abstract contract ERC1155Pods is ERC1155, IERC1155Pods, ReentrancyGuardExt { + using TokenPodsLib for TokenPodsLib.Data; + using ReentrancyGuardLib for ReentrancyGuardLib.Data; + + error ZeroPodsLimit(); + error PodsLimitReachedForAccount(); + + uint256 public immutable podsLimit; + uint256 public immutable podCallGasLimit; + + ReentrancyGuardLib.Data private _guard; + mapping(uint256 => TokenPodsLib.Data) private _pods; + + constructor(uint256 podsLimit_, uint256 podCallGasLimit_) { + if (podsLimit_ == 0) revert ZeroPodsLimit(); + podsLimit = podsLimit_; + podCallGasLimit = podCallGasLimit_; + _guard.init(); + } + + function hasPod(address account, address pod, uint256 id) public view virtual returns(bool) { + return _pods[id].hasPod(account, pod); + } + + function podsCount(address account, uint256 id) public view virtual returns(uint256) { + return _pods[id].podsCount(account); + } + + function podAt(address account, uint256 index, uint256 id) public view virtual returns(address) { + return _pods[id].podAt(account, index); + } + + function pods(address account, uint256 id) public view virtual returns(address[] memory) { + return _pods[id].pods(account); + } + + function balanceOf(address account, uint256 id) public nonReentrantView(_guard) view override(IERC1155, ERC1155) virtual returns(uint256) { + return super.balanceOf(account, id); + } + + function podBalanceOf(address pod, address account, uint256 id) public nonReentrantView(_guard) view returns(uint256) { + return _pods[id].podBalanceOf(account, pod, super.balanceOf(msg.sender, id)); + } + + function addPod(address pod, uint256 id) public virtual { + if (_pods[id].addPod(msg.sender, pod, balanceOf(msg.sender, id), podCallGasLimit) > podsLimit) revert PodsLimitReachedForAccount(); + } + + function removePod(address pod, uint256 id) public virtual { + _pods[id].removePod(msg.sender, pod, balanceOf(msg.sender, id), podCallGasLimit); + } + + function removeAllPods(uint256 id) public virtual { + _pods[id].removeAllPods(msg.sender, balanceOf(msg.sender, id), podCallGasLimit); + } + + // ERC1155 Overrides + + function _afterTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal nonReentrant(_guard) override virtual { + super._afterTokenTransfer(operator, from, to, ids, amounts, data); + + unchecked { + for (uint256 i = 0; i < ids.length; i++) { + _pods[i].updateBalancesWithTokenId(from, to, amounts[i], ids[i], podCallGasLimit); + } + } + } +} diff --git a/contracts/ERC20Pods.sol b/contracts/ERC20Pods.sol index 16c060a..e6d61fb 100644 --- a/contracts/ERC20Pods.sol +++ b/contracts/ERC20Pods.sol @@ -6,26 +6,21 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@1inch/solidity-utils/contracts/libraries/AddressSet.sol"; import "./interfaces/IERC20Pods.sol"; -import "./interfaces/IPod.sol"; +import "./TokenPodsLib.sol"; import "./libs/ReentrancyGuard.sol"; abstract contract ERC20Pods is ERC20, IERC20Pods, ReentrancyGuardExt { - using AddressSet for AddressSet.Data; - using AddressArray for AddressArray.Data; + using TokenPodsLib for TokenPodsLib.Data; using ReentrancyGuardLib for ReentrancyGuardLib.Data; - error PodAlreadyAdded(); - error PodNotFound(); - error InvalidPodAddress(); - error PodsLimitReachedForAccount(); - error InsufficientGas(); error ZeroPodsLimit(); + error PodsLimitReachedForAccount(); uint256 public immutable podsLimit; uint256 public immutable podCallGasLimit; ReentrancyGuardLib.Data private _guard; - mapping(address => AddressSet.Data) private _pods; + TokenPodsLib.Data private _pods; constructor(uint256 podsLimit_, uint256 podCallGasLimit_) { if (podsLimit_ == 0) revert ZeroPodsLimit(); @@ -35,19 +30,19 @@ abstract contract ERC20Pods is ERC20, IERC20Pods, ReentrancyGuardExt { } function hasPod(address account, address pod) public view virtual returns(bool) { - return _pods[account].contains(pod); + return _pods.hasPod(account, pod); } function podsCount(address account) public view virtual returns(uint256) { - return _pods[account].length(); + return _pods.podsCount(account); } function podAt(address account, uint256 index) public view virtual returns(address) { - return _pods[account].at(index); + return _pods.podAt(account, index); } function pods(address account) public view virtual returns(address[] memory) { - return _pods[account].items.get(); + return _pods.pods(account); } function balanceOf(address account) public nonReentrantView(_guard) view override(IERC20, ERC20) virtual returns(uint256) { @@ -55,121 +50,25 @@ abstract contract ERC20Pods is ERC20, IERC20Pods, ReentrancyGuardExt { } function podBalanceOf(address pod, address account) public nonReentrantView(_guard) view virtual returns(uint256) { - if (hasPod(account, pod)) { - return super.balanceOf(account); - } - return 0; + return _pods.podBalanceOf(account, pod, super.balanceOf(account)); } function addPod(address pod) public virtual { - _addPod(msg.sender, pod); + if (_pods.addPod(msg.sender, pod, balanceOf(msg.sender), podCallGasLimit) > podsLimit) revert PodsLimitReachedForAccount(); } function removePod(address pod) public virtual { - _removePod(msg.sender, pod); + _pods.removePod(msg.sender, pod, balanceOf(msg.sender), podCallGasLimit); } function removeAllPods() public virtual { - _removeAllPods(msg.sender); - } - - function _addPod(address account, address pod) internal virtual { - if (pod == address(0)) revert InvalidPodAddress(); - if (!_pods[account].add(pod)) revert PodAlreadyAdded(); - if (_pods[account].length() > podsLimit) revert PodsLimitReachedForAccount(); - - emit PodAdded(account, pod); - uint256 balance = balanceOf(account); - if (balance > 0) { - _updateBalances(pod, address(0), account, balance); - } - } - - function _removePod(address account, address pod) internal virtual { - if (!_pods[account].remove(pod)) revert PodNotFound(); - - emit PodRemoved(account, pod); - uint256 balance = balanceOf(account); - if (balance > 0) { - _updateBalances(pod, account, address(0), balance); - } - } - - function _removeAllPods(address account) internal virtual { - address[] memory items = _pods[account].items.get(); - uint256 balance = balanceOf(account); - unchecked { - for (uint256 i = items.length; i > 0; i--) { - _pods[account].remove(items[i - 1]); - emit PodRemoved(account, items[i - 1]); - if (balance > 0) { - _updateBalances(items[i - 1], account, address(0), balance); - } - } - } - } - - /// @notice Assembly implementation of the gas limited call to avoid return gas bomb, - // moreover call to a destructed pod would also revert even inside try-catch block in Solidity 0.8.17 - /// @dev try IPod(pod).updateBalances{gas: _POD_CALL_GAS_LIMIT}(from, to, amount) {} catch {} - function _updateBalances(address pod, address from, address to, uint256 amount) private { - bytes4 selector = IPod.updateBalances.selector; - bytes4 exception = InsufficientGas.selector; - uint256 gasLimit = podCallGasLimit; - assembly { // solhint-disable-line no-inline-assembly - let ptr := mload(0x40) - mstore(ptr, selector) - mstore(add(ptr, 0x04), from) - mstore(add(ptr, 0x24), to) - mstore(add(ptr, 0x44), amount) - - if lt(div(mul(gas(), 63), 64), gasLimit) { - mstore(0, exception) - revert(0, 4) - } - pop(call(gasLimit, pod, 0, ptr, 0x64, 0, 0)) - } + _pods.removeAllPods(msg.sender, balanceOf(msg.sender), podCallGasLimit); } // ERC20 Overrides function _afterTokenTransfer(address from, address to, uint256 amount) internal nonReentrant(_guard) override virtual { super._afterTokenTransfer(from, to, amount); - - unchecked { - if (amount > 0 && from != to) { - address[] memory a = _pods[from].items.get(); - address[] memory b = _pods[to].items.get(); - uint256 aLength = a.length; - uint256 bLength = b.length; - - for (uint256 i = 0; i < aLength; i++) { - address pod = a[i]; - - uint256 j; - for (j = 0; j < bLength; j++) { - if (pod == b[j]) { - // Both parties are participating of the same Pod - _updateBalances(pod, from, to, amount); - b[j] = address(0); - break; - } - } - - if (j == bLength) { - // Sender is participating in a Pod, but receiver is not - _updateBalances(pod, from, address(0), amount); - } - } - - for (uint256 j = 0; j < bLength; j++) { - address pod = b[j]; - if (pod != address(0)) { - // Receiver is participating in a Pod, but sender is not - _updateBalances(pod, address(0), to, amount); - } - } - } - } + _pods.updateBalances(from, to, amount, podCallGasLimit); } } diff --git a/contracts/ERC721Pods.sol b/contracts/ERC721Pods.sol new file mode 100644 index 0000000..1ee301f --- /dev/null +++ b/contracts/ERC721Pods.sol @@ -0,0 +1,75 @@ + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@1inch/solidity-utils/contracts/libraries/AddressSet.sol"; + +import "./interfaces/IERC721Pods.sol"; +import "./TokenPodsLib.sol"; +import "./libs/ReentrancyGuard.sol"; + +abstract contract ERC721Pods is ERC721, IERC721Pods, ReentrancyGuardExt { + using TokenPodsLib for TokenPodsLib.Data; + using ReentrancyGuardLib for ReentrancyGuardLib.Data; + + error ZeroPodsLimit(); + error PodsLimitReachedForAccount(); + + uint256 public immutable podsLimit; + uint256 public immutable podCallGasLimit; + + ReentrancyGuardLib.Data private _guard; + TokenPodsLib.Data private _pods; + + constructor(uint256 podsLimit_, uint256 podCallGasLimit_) { + if (podsLimit_ == 0) revert ZeroPodsLimit(); + podsLimit = podsLimit_; + podCallGasLimit = podCallGasLimit_; + _guard.init(); + } + + function hasPod(address account, address pod) public view virtual returns(bool) { + return _pods.hasPod(account, pod); + } + + function podsCount(address account) public view virtual returns(uint256) { + return _pods.podsCount(account); + } + + function podAt(address account, uint256 index) public view virtual returns(address) { + return _pods.podAt(account, index); + } + + function pods(address account) public view virtual returns(address[] memory) { + return _pods.pods(account); + } + + function balanceOf(address account) public nonReentrantView(_guard) view override(IERC721, ERC721) virtual returns(uint256) { + return super.balanceOf(account); + } + + function podBalanceOf(address pod, address account) public nonReentrantView(_guard) view virtual returns(uint256) { + return _pods.podBalanceOf(account, pod, super.balanceOf(account)); + } + + function addPod(address pod) public virtual { + if (_pods.addPod(msg.sender, pod, balanceOf(msg.sender), podCallGasLimit) > podsLimit) revert PodsLimitReachedForAccount(); + } + + function removePod(address pod) public virtual { + _pods.removePod(msg.sender, pod, balanceOf(msg.sender), podCallGasLimit); + } + + function removeAllPods() public virtual { + _pods.removeAllPods(msg.sender, balanceOf(msg.sender), podCallGasLimit); + } + + // ERC721 Overrides + + function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal nonReentrant(_guard) override virtual { + super._afterTokenTransfer(from, to, firstTokenId, batchSize); + _pods.updateBalances(from, to, batchSize, podCallGasLimit); + } +} diff --git a/contracts/Pod.sol b/contracts/Pod.sol index 92c6564..06adc62 100644 --- a/contracts/Pod.sol +++ b/contracts/Pod.sol @@ -3,25 +3,37 @@ pragma solidity ^0.8.0; import "./interfaces/IPod.sol"; +import "./interfaces/IPodWithId.sol"; import "./interfaces/IERC20Pods.sol"; -abstract contract Pod is IPod { +abstract contract Pod is IPod, IPodWithId { error AccessDenied(); IERC20Pods public immutable token; + uint256 public immutable tokenId; modifier onlyToken { if (msg.sender != address(token)) revert AccessDenied(); _; } - constructor(IERC20Pods token_) { + modifier onlyTokenId(uint256 id) { + if (id != tokenId) revert AccessDenied(); + _; + } + + constructor(IERC20Pods token_, uint256 tokenId_) { token = token_; + tokenId = tokenId_; } function updateBalances(address from, address to, uint256 amount) external onlyToken { _updateBalances(from, to, amount); } + function updateBalancesWithTokenId(address from, address to, uint256 amount, uint256 id) external onlyToken onlyTokenId(id) { + _updateBalances(from, to, amount); + } + function _updateBalances(address from, address to, uint256 amount) internal virtual; } diff --git a/contracts/TokenPodsLib.sol b/contracts/TokenPodsLib.sol new file mode 100644 index 0000000..e5883fc --- /dev/null +++ b/contracts/TokenPodsLib.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@1inch/solidity-utils/contracts/libraries/AddressSet.sol"; + +import "./interfaces/IPod.sol"; +import "./interfaces/IPodWithId.sol"; + +library TokenPodsLib { + using AddressSet for AddressSet.Data; + using AddressArray for AddressArray.Data; + + error PodAlreadyAdded(); + error PodNotFound(); + error InvalidPodAddress(); + error InsufficientGas(); + + event PodAdded(address account, address pod); + event PodRemoved(address account, address pod); + + type DataPtr is uint256; + + struct Data { + mapping(address => AddressSet.Data) _pods; + } + + function hasPod(Data storage self, address account, address pod) internal view returns(bool) { + return self._pods[account].contains(pod); + } + + function podsCount(Data storage self, address account) internal view returns(uint256) { + return self._pods[account].length(); + } + + function podAt(Data storage self, address account, uint256 index) internal view returns(address) { + return self._pods[account].at(index); + } + + function pods(Data storage self, address account) internal view returns(address[] memory) { + return self._pods[account].items.get(); + } + + function podBalanceOf(Data storage self, address account, address pod, uint256 balance) internal view returns(uint256) { + if (self._pods[account].contains(pod)) { + return balance; + } + return 0; + } + + function addPod(Data storage self, address account, address pod, uint256 balance, uint256 podCallGasLimit) internal returns(uint256) { + return _addPod(self, account, pod, balance, podCallGasLimit); + } + + function removePod(Data storage self, address account, address pod, uint256 balance, uint256 podCallGasLimit) internal { + _removePod(self, account, pod, balance, podCallGasLimit); + } + + function removeAllPods(Data storage self, address account, uint256 balance, uint256 podCallGasLimit) internal { + _removeAllPods(self, account, balance, podCallGasLimit); + } + + function _addPod(Data storage self, address account, address pod, uint256 balance, uint256 podCallGasLimit) private returns(uint256) { + if (pod == address(0)) revert InvalidPodAddress(); + if (!self._pods[account].add(pod)) revert PodAlreadyAdded(); + + emit PodAdded(account, pod); + if (balance > 0) { + _notifyPod(pod, address(0), account, balance, 0, false, podCallGasLimit); + } + return self._pods[account].length(); + } + + function _removePod(Data storage self, address account, address pod, uint256 balance, uint256 podCallGasLimit) private { + if (!self._pods[account].remove(pod)) revert PodNotFound(); + if (balance > 0) { + _notifyPod(pod, account, address(0), balance, 0, false, podCallGasLimit); + } + } + + function _removeAllPods(Data storage self, address account, uint256 balance, uint256 podCallGasLimit) private { + address[] memory items = self._pods[account].items.get(); + unchecked { + for (uint256 i = items.length; i > 0; i--) { + self._pods[account].remove(items[i - 1]); + emit PodRemoved(account, items[i - 1]); + if (balance > 0) { + _notifyPod(items[i - 1], account, address(0), balance, 0, false, podCallGasLimit); + } + } + } + } + + function updateBalances(Data storage self, address from, address to, uint256 amount, uint256 podCallGasLimit) internal { + _updateBalances(self, from, to, amount, 0, false, podCallGasLimit); + } + + function updateBalancesWithTokenId(Data storage self, address from, address to, uint256 amount, uint256 id, uint256 podCallGasLimit) internal { + _updateBalances(self, from, to, amount, id, true, podCallGasLimit); + } + + function _updateBalances(Data storage self, address from, address to, uint256 amount, uint256 id, bool hasId, uint256 podCallGasLimit) private { + unchecked { + if (amount > 0 && from != to) { + address[] memory a = self._pods[from].items.get(); + address[] memory b = self._pods[to].items.get(); + uint256 aLength = a.length; + uint256 bLength = b.length; + + for (uint256 i = 0; i < aLength; i++) { + address pod = a[i]; + + uint256 j; + for (j = 0; j < bLength; j++) { + if (pod == b[j]) { + // Both parties are participating of the same Pod + _notifyPod(pod, from, to, amount, id, hasId, podCallGasLimit); + b[j] = address(0); + break; + } + } + + if (j == bLength) { + // Sender is participating in a Pod, but receiver is not + _notifyPod(pod, from, address(0), amount, id, hasId, podCallGasLimit); + } + } + + for (uint256 j = 0; j < bLength; j++) { + address pod = b[j]; + if (pod != address(0)) { + // Receiver is participating in a Pod, but sender is not + _notifyPod(pod, address(0), to, amount, id, hasId, podCallGasLimit); + } + } + } + } + } + + /// @notice Assembly implementation of the gas limited call to avoid return gas bomb, + // moreover call to a destructed pod would also revert even inside try-catch block in Solidity 0.8.17 + /// @dev try IPod(pod).updateBalances{gas: _POD_CALL_GAS_LIMIT}(from, to, amount) {} catch {} + function _notifyPod(address pod, address from, address to, uint256 amount, uint256 id, bool hasId, uint256 podCallGasLimit) private { + bytes4 selector = hasId ? IPodWithId.updateBalancesWithTokenId.selector : IPod.updateBalances.selector; + bytes4 exception = InsufficientGas.selector; + assembly { // solhint-disable-line no-inline-assembly + let ptr := mload(0x40) + mstore(ptr, selector) + mstore(add(ptr, 0x04), from) + mstore(add(ptr, 0x24), to) + mstore(add(ptr, 0x44), amount) + if hasId { + mstore(add(ptr, 0x64), id) + } + + if lt(div(mul(gas(), 63), 64), podCallGasLimit) { + mstore(0, exception) + revert(0, 4) + } + pop(call(podCallGasLimit, pod, 0, ptr, add(0x64, mul(hasId, 0x20)), 0, 0)) + } + } +} diff --git a/contracts/interfaces/IERC1155Pods.sol b/contracts/interfaces/IERC1155Pods.sol new file mode 100644 index 0000000..eb85512 --- /dev/null +++ b/contracts/interfaces/IERC1155Pods.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +interface IERC1155Pods is IERC1155 { + event PodAdded(address account, address pod); + event PodRemoved(address account, address pod); + + function hasPod(address account, address pod, uint256 id) external view returns(bool); + function podsCount(address account, uint256 id) external view returns(uint256); + function podAt(address account, uint256 index, uint256 id) external view returns(address); + function pods(address account, uint256 id) external view returns(address[] memory); + function podBalanceOf(address pod, address account, uint256 id) external view returns(uint256); + + function addPod(address pod, uint256 id) external; + function removePod(address pod, uint256 id) external; + function removeAllPods(uint256 id) external; +} diff --git a/contracts/interfaces/IERC721Pods.sol b/contracts/interfaces/IERC721Pods.sol new file mode 100644 index 0000000..cc2bc80 --- /dev/null +++ b/contracts/interfaces/IERC721Pods.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +interface IERC721Pods is IERC721 { + event PodAdded(address account, address pod); + event PodRemoved(address account, address pod); + + function hasPod(address account, address pod) external view returns(bool); + function podsCount(address account) external view returns(uint256); + function podAt(address account, uint256 index) external view returns(address); + function pods(address account) external view returns(address[] memory); + function podBalanceOf(address pod, address account) external view returns(uint256); + + function addPod(address pod) external; + function removePod(address pod) external; + function removeAllPods() external; +} diff --git a/contracts/interfaces/IPod.sol b/contracts/interfaces/IPod.sol index 52ffa0f..4f150e5 100644 --- a/contracts/interfaces/IPod.sol +++ b/contracts/interfaces/IPod.sol @@ -2,6 +2,12 @@ pragma solidity ^0.8.0; +/// @title Pod interface interface IPod { + /// Pod receives notifications about balance changes of participants + /// @dev This function implementation should make sure `msg.sender` is designated token of this Pod + /// @param from The address of the sender or `address(0)` if the transfer is a mint or sender is not participating in this Pod + /// @param to The address of the recipient or `address(0)` if the transfer is a burn or recipient is not participating in this Pod + /// @param amount The amount of tokens being transferred function updateBalances(address from, address to, uint256 amount) external; } diff --git a/contracts/interfaces/IPodWithId.sol b/contracts/interfaces/IPodWithId.sol new file mode 100644 index 0000000..7952a88 --- /dev/null +++ b/contracts/interfaces/IPodWithId.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @title Pod interface for tokens of EIP-1155 standard +interface IPodWithId { + /// Pod receives notifications about balance changes of participants + /// @dev This function implementation MUST make sure `msg.sender` is designated token of this Pod + /// @dev This function implementation MUST make sure `id` argument is designated token id of this Pod + /// @param from The address of the sender or `address(0)` if the transfer is a mint or sender is not participating in this Pod + /// @param to The address of the recipient or `address(0)` if the transfer is a burn or recipient is not participating in this Pod + /// @param id The EIP-1155 `token_id` of the token being transferred + /// @param amount The amount of tokens being transferred + function updateBalancesWithTokenId(address from, address to, uint256 amount, uint256 id) external; +} diff --git a/contracts/mocks/PodMock.sol b/contracts/mocks/PodMock.sol index 5822616..2b0d96f 100644 --- a/contracts/mocks/PodMock.sol +++ b/contracts/mocks/PodMock.sol @@ -6,9 +6,13 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../Pod.sol"; contract PodMock is ERC20, Pod { - constructor(string memory name, string memory symbol, IERC20Pods token_) ERC20(name, symbol) Pod(token_) {} // solhint-disable-line no-empty-blocks + constructor(string memory name, string memory symbol, IERC20Pods token_) + ERC20(name, symbol) + Pod(token_, 0) + {} // solhint-disable-line no-empty-blocks function _updateBalances(address from, address to, uint256 amount) internal override { + // Replicate balances if (from == address(0)) { _mint(to, amount); } else if (to == address(0)) { diff --git a/contracts/mocks/WrongPodMock.sol b/contracts/mocks/WrongPodMock.sol index e19a3ea..39e25b5 100644 --- a/contracts/mocks/WrongPodMock.sol +++ b/contracts/mocks/WrongPodMock.sol @@ -12,9 +12,12 @@ contract WrongPodMock is ERC20, Pod { bool public isOutOfGas; bool public isReturnGasBomb; - constructor(string memory name, string memory symbol, IERC20Pods token_) ERC20(name, symbol) Pod(token_) {} // solhint-disable-line no-empty-blocks + constructor(string memory name, string memory symbol, IERC20Pods token_) + ERC20(name, symbol) + Pod(token_, 0) + {} // solhint-disable-line no-empty-blocks - function _updateBalances(address /*from*/, address /*to*/, uint256 /*amount*/) internal view override { + function _updateBalances(address /* from */, address /* to */, uint256 /* amount */) internal view override { if (isRevert) revert PodsUpdateBalanceRevert(); if (isOutOfGas) assert(false); if (isReturnGasBomb) { assembly { return(0, 1000000) } } // solhint-disable-line no-inline-assembly diff --git a/package.json b/package.json index 0d1169f..4aefa52 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { - "name": "@1inch/erc20-pods", - "version": "0.0.16", + "name": "@1inch/token-pods", + "version": "0.1.0", "description": "ERC20 extension enabling external smart contract based Pods to track balances of those users who opted-in to these Pods", "repository": { "type": "git", - "url": "git@github.com:1inch/erc20-pods.git" + "url": "git@github.com:1inch/token-pods.git" }, "bugs": { - "url": "https://github.com/1inch/erc20-pods/issues" + "url": "https://github.com/1inch/token-pods/issues" }, - "homepage": "https://github.com/1inch/erc20-pods#readme", + "homepage": "https://github.com/1inch/token-pods#readme", "author": "1inch", "license": "MIT", "dependencies": { diff --git a/test/ERC20Pods.js b/test/ERC20Pods.js index 369ac92..3269a4d 100644 --- a/test/ERC20Pods.js +++ b/test/ERC20Pods.js @@ -46,7 +46,7 @@ describe('ERC20Pods', function () { await wrongPod.setReturnGasBomb(true); const tx = await erc20Pods.addPod(wrongPod.address); const receipt = await tx.wait(); - expect(receipt.gasUsed).to.be.lt(275761); + expect(receipt.gasUsed).to.be.lt(275901); expect(await erc20Pods.pods(wallet1.address)).to.have.deep.equals([wrongPod.address]); }); }); diff --git a/test/behaviors/ERC20Pods.behavior.js b/test/behaviors/ERC20Pods.behavior.js index 2b94f93..d9f4b08 100644 --- a/test/behaviors/ERC20Pods.behavior.js +++ b/test/behaviors/ERC20Pods.behavior.js @@ -151,7 +151,7 @@ function shouldBehaveLikeERC20Pods (initContracts) { expect(await erc20Pods.hasPod(wallet1.address, pods[1].address)).to.be.equals(false); await erc20Pods.addPod(pods[0].address); await erc20Pods.addPod(pods[1].address); - expect(await erc20Pods.pods(wallet1.address)).to.have.deep.equals([pods[0].address, pods[1].address]); + expect(await erc20Pods.pods(wallet1.address)).to.be.deep.equals([pods[0].address, pods[1].address]); }); it('should updateBalance via pod only for wallets with non-zero balance', async function () { @@ -232,14 +232,14 @@ function shouldBehaveLikeERC20Pods (initContracts) { const { erc20Pods, wrongPod } = await loadFixture(initWrongPodAndMint); await wrongPod.setIsRevert(true); await erc20Pods.addPod(wrongPod.address); - expect(await erc20Pods.pods(wallet1.address)).to.have.deep.equals([wrongPod.address]); + expect(await erc20Pods.pods(wallet1.address)).to.be.deep.equals([wrongPod.address]); }); it('should not fail when updateBalance in pod has OutOfGas', async function () { const { erc20Pods, wrongPod } = await loadFixture(initWrongPodAndMint); await wrongPod.setOutOfGas(true); await erc20Pods.addPod(wrongPod.address); - expect(await erc20Pods.pods(wallet1.address)).to.have.deep.equals([wrongPod.address]); + expect(await erc20Pods.pods(wallet1.address)).to.be.deep.equals([wrongPod.address]); }); });