diff --git a/contracts/examples/dao/DAO.sol b/contracts/examples/dao/DAO.sol index f1160f4..89cd01b 100644 --- a/contracts/examples/dao/DAO.sol +++ b/contracts/examples/dao/DAO.sol @@ -44,7 +44,7 @@ contract DAO is VentureEth, Democratic { /** * @dev Fallback function. Required when collecting ether dividends from ventures. */ - receive() external virtual payable {} + receive() external virtual override payable {} /** * @notice To be called during the first investment round. @@ -66,7 +66,7 @@ contract DAO is VentureEth, Democratic { * @param investment The ether to invest in the venture. */ function investVenture( - address venture, + address payable venture, uint256 investment ) public virtual onlyProposal { ventures.add(venture); @@ -79,7 +79,7 @@ contract DAO is VentureEth, Democratic { * @param venture The address of the VentureEth contract to retrieve tokens from. */ function retrieveVentureTokens( - address venture + address payable venture ) public virtual { VentureEth(venture).claim(); } @@ -89,7 +89,7 @@ contract DAO is VentureEth, Democratic { * @param venture The address of the VentureEth contract from which to cancel the investment. */ function cancelVenture( - address venture + address payable venture ) public virtual onlyProposal { VentureEth(venture).cancelInvestment(); ventures.remove(venture); @@ -100,7 +100,7 @@ contract DAO is VentureEth, Democratic { * @param venture The venture to claim dividends from. */ function claimDividendsFromVenture( - address venture + address payable venture ) public virtual returns(uint256) { return VentureEth(venture).claimDividends(); } diff --git a/contracts/test/token/TestERC20DividendableEth.sol b/contracts/test/token/TestERC20DividendableEth.sol index bd0758d..11aedfc 100644 --- a/contracts/test/token/TestERC20DividendableEth.sol +++ b/contracts/test/token/TestERC20DividendableEth.sol @@ -15,4 +15,11 @@ contract TestERC20DividendableEth is ERC20DividendableEth, ERC20Burnable function testReleaseDividends(uint256 amount) public virtual { _releaseDividends(amount); } + + /// @dev There is some bug. If you override a function you seem to need to override it in all derived contracts. + function _beforeTokenTransfer(address from, address to, uint256 amount) + internal override(ERC20, ERC20DividendableEth) + { + super._beforeTokenTransfer(from, to, amount); + } } \ No newline at end of file diff --git a/contracts/token/ERC20DividendableEth.sol b/contracts/token/ERC20DividendableEth.sol index 8881327..f046bd6 100644 --- a/contracts/token/ERC20DividendableEth.sol +++ b/contracts/token/ERC20DividendableEth.sol @@ -1,8 +1,8 @@ pragma solidity ^0.6.0; -import "@openzeppelin/contracts/math/SafeMath.sol"; import "./ERC20MintableDetailed.sol"; import "../math/DecimalMath.sol"; +import "../utils/SafeCast.sol"; /** @@ -11,65 +11,79 @@ import "../math/DecimalMath.sol"; * @notice This contract was implemented from algorithms proposed by Nick Johnson here: https://medium.com/@weka/dividend-bearing-tokens-on-ethereum-42d01c710657 */ contract ERC20DividendableEth is ERC20MintableDetailed { - - using SafeMath for uint256; + using DecimalMath for int256; using DecimalMath for uint256; + using SafeCast for int256; + using SafeCast for uint256; + + int256 public dividendsPerToken; + mapping(address => int256) private claimedDPT; - uint256 public dividendsPerToken; // This is a decimal number - mapping(address => uint256) public lastDPT; // These are decimal numbers + constructor(string memory name, string memory symbol, uint8 decimals) + ERC20MintableDetailed(name, symbol, decimals) public + {} - constructor( - string memory name, - string memory symbol, - uint8 decimals - ) ERC20MintableDetailed(name, symbol, decimals) public {} + /// @dev Receive function + receive() external virtual payable {} - /** - * @notice Send ether to this function in orther to disburse dividends - */ - function releaseDividends() external virtual payable { + /// @dev Send ether to this function in order to release dividends + function releaseDividends() + external virtual payable + { _releaseDividends(msg.value); } - /** - * @dev Function to update the account of the sender - * @notice Will revert if account need not be updated - */ - function claimDividends() public virtual returns(uint256) { + /// @dev Function to update the account of the sender + /// @notice Will revert if account need not be updated + function claimDividends() + public virtual returns(uint256) + { return _claimDividends(msg.sender); } - /** - * @dev Release an `amount` of ether in the contract as dividends. - */ - function _releaseDividends(uint256 amount) internal { + /// @dev Release an `amount` of ether in the contract as dividends. + function _releaseDividends(uint256 amount) + internal + { require(address(this).balance >= amount, "Not enough funds."); - // Wei amounts are already decimals. - uint256 releasedDPT = amount.divd(this.totalSupply()); + int256 releasedDPT = amount.divd(this.totalSupply()).toInt(); dividendsPerToken = dividendsPerToken.addd(releasedDPT); + claimedDPT[address(0)] = dividendsPerToken; // Mint tokens at DPT } - /** - * @dev Transfer owed dividends to its account. - */ + /// @dev Transfer owed dividends to its account. function _claimDividends(address payable account) - internal - returns(uint256) + internal returns(uint256) { uint256 owing = _dividendsOwing(account); require(owing > 0, "Account need not be updated now."); account.transfer(owing); - lastDPT[account] = dividendsPerToken; + claimedDPT[account] = dividendsPerToken; return owing; } - /** - * @dev Internal function to compute dividends owing to an account - * @param account The account for which to compute the dividends - */ - function _dividendsOwing(address account) internal view returns(uint256) { - uint256 owedDPT = dividendsPerToken.subd(lastDPT[account]); - return this.balanceOf(account).muld(owedDPT); + /// @dev Internal function to compute dividends owing to an account + /// @param account The account for which to compute the dividends + function _dividendsOwing(address account) + internal view returns(uint256) + { + int256 owedDPT = dividendsPerToken.subd(claimedDPT[account]); + return owedDPT.toUint().muld(this.balanceOf(account)); } + /// @dev Add to the adjustment DPT the weighted average between the recipient's balance DPT, and the transfer tokens DPT + function _beforeTokenTransfer(address from, address to, uint256 amount) + internal virtual override + { + // If burning, do nothing + if (to == address(0)) return; + + // If transferring to an empty account, reset its claimed DTP + if (this.balanceOf(to) == 0) delete claimedDPT[to]; + + int256 weight = amount.divd(this.balanceOf(to).addd(amount)).toInt(); + int256 differentialDPT = claimedDPT[from].subd(claimedDPT[to]); + int256 weightedDPT = differentialDPT.muld(weight); + claimedDPT[to] = claimedDPT[to].addd(weightedDPT); + } } \ No newline at end of file diff --git a/contracts/token/README.md b/contracts/token/README.md index 5731dfb..b45ab60 100644 --- a/contracts/token/README.md +++ b/contracts/token/README.md @@ -24,17 +24,14 @@ It is a contract that implements the `IERC20MintableDetailed` interface. It is a `ERC20` token contract that is endowed with some rather dividendable qualities. -1. Anyone can send `ether` to the contract at any time using `increasePool`. That amount of `ether` will be added to a dividend pool. +1. Anyone can send `ether` to the contract at any time using the `receive` function. That amount of `ether` will be added to a dividend pool. -2. Any token holder can draw their fair share of `ether` from the dividend pool according to the amount of tokens they hold. To do this they must call the `updateAccount` function. +2. The contract has an internal `_releaseDividends` function that will earmark a portion of the `ether` in the contract to be claimed by token holders proportionally to their holdings. -#### Notes -1. Changes in the token supply will affect any dividend distribution events. Any ongoing distribution events for which the contract has received the `ether` before the total supply change are unaffected. +3. Any token holder can claim their share of `ether` dividends calling the `claimDividends` function. -2. In order to be useful in practice, the contract has to be customized by inheriting from a mintable standard implementation, for example, openzeppelin's `ERC20Mintable` contract, in this way: +4. A token holder can transfer tokens while having dividends available for claiming. In that case, only the recipient can claim the share of dividends related to the tokens transferred. -``` -contract MyERC20DividendableEth is ERC20DividendableEth, ERC20Mintable { - // here goes your fantasy -} -``` +5. Minted tokens don't give any right to claim dividends from prior events. + +6. Burning tokens while having dividends available for claiming proportionally reduces the dividends that can be claimed. \ No newline at end of file diff --git a/contracts/utils/SafeCast.sol b/contracts/utils/SafeCast.sol new file mode 100644 index 0000000..5155e0c --- /dev/null +++ b/contracts/utils/SafeCast.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.6.0; + + +/// @dev Implements safe casting between int256 and uint256 +/// @author Alberto Cuesta CaƱada +library SafeCast { + + /// @dev Maximum value that can be represented in an int256 + function maxInt256() internal pure returns(int256) { + // solium-disable-next-line max-len + return 57896044618658097711785492504343953926634992332820282019728792003956564819967; + } + + /// @dev Safe casting from int256 to uint256 + function toUint(int256 x) internal pure returns(uint256) { + require( + x >= 0, + "Cannot cast negative signed integer to unsigned integer." + ); + return uint256(x); + } + + /// @dev Safe casting from uint256 to int256 + function toInt(uint256 x) internal pure returns(int256) { + require( + x <= toUint(maxInt256()), + "Cannot cast overflowing unsigned integer to signed integer." + ); + return int256(x); + } +} diff --git a/test/token/ERC20DividendableEth.test.ts b/test/token/ERC20DividendableEth.test.ts index d7e9283..25cb4c2 100644 --- a/test/token/ERC20DividendableEth.test.ts +++ b/test/token/ERC20DividendableEth.test.ts @@ -67,17 +67,56 @@ contract('ERC20DividendableEth', (accounts) => { BN(await erc20dividendableEth.claimDividends.call({ from: account2 })) .should.be.bignumber.equal(claimedDividends2); }); - }); - /** - * @test {ERC20DividendableEth#claimDividends} - */ - it('dividends can be claimed after minting tokens', async () => { - await erc20dividendableEth.releaseDividends({ from: user1, value: releasedDividends.toString()}); - await erc20dividendableEth.mint(account2, balance1.add(balance2)); - await erc20dividendableEth.releaseDividends({ from: user1, value: releasedDividends.toString()}); - BN(await erc20dividendableEth.claimDividends.call({ from: account1 })) + /** + * @test {ERC20DividendableEth#claimDividends} + */ + it('dividends per token are adjusted downwards after minting tokens', async () => { + await erc20dividendableEth.mint(account2, balance1.add(balance2)); + BN(await erc20dividendableEth.claimDividends.call({ from: account1 })) + .should.be.bignumber.equal(claimedDividends1); + BN(await erc20dividendableEth.claimDividends.call({ from: account2 })) .should.be.bignumber.equal(claimedDividends2); + }); + + /** + * @test {ERC20DividendableEth#claimDividends} + */ + it('dividends per token remain constant after burning tokens', async () => { + await erc20dividendableEth.burn(balance2.div(new BN('2')), { from: account2 }); + BN(await erc20dividendableEth.claimDividends.call({ from: account2 })) + .should.be.bignumber.equal(claimedDividends2.div(new BN('2'))); + }); + + /** + * @test {ERC20DividendableEth#claimDividends} + */ + it('dividends can be claimed after transferring tokens', async () => { + await erc20dividendableEth.transfer(account2, balance1, { from: account1 }); + await expectRevert(erc20dividendableEth.claimDividends({ from: account1 }), 'Account need not be updated now.'); + BN(await erc20dividendableEth.claimDividends.call({ from: account2 })) + .should.be.bignumber.equal(claimedDividends1.add(claimedDividends2)); + }); + + /** + * @test {ERC20DividendableEth#claimDividends} + */ + it('dividends per token are adjusted downwards after transferring tokens', async () => { + await erc20dividendableEth.claimDividends({ from: account1 }) + await erc20dividendableEth.transfer(account2, balance1, { from: account1 }); + BN(await erc20dividendableEth.claimDividends.call({ from: account2 })) + .should.be.bignumber.equal(claimedDividends2); + }); + + /** + * @test {ERC20DividendableEth#claimDividends} + */ + it('dividends per token are adjusted upwards after transferring tokens', async () => { + await erc20dividendableEth.claimDividends({ from: account2 }) + await erc20dividendableEth.transfer(account2, balance1, { from: account1 }); + BN(await erc20dividendableEth.claimDividends.call({ from: account2 })) + .should.be.bignumber.equal(claimedDividends1); + }); }); /**