diff --git a/Mainnet/DividendToken/DividendToken.Tests/AddressExtensions.cs b/Mainnet/DividendToken/DividendToken.Tests/AddressExtensions.cs new file mode 100644 index 00000000..28365d9f --- /dev/null +++ b/Mainnet/DividendToken/DividendToken.Tests/AddressExtensions.cs @@ -0,0 +1,42 @@ +using Stratis.SmartContracts; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DividendTokenContract.Tests +{ + public static class AddressExtensions + { + private static byte[] HexStringToBytes(string val) + { + if (val.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + val = val.Substring(2); + + byte[] ret = new byte[val.Length / 2]; + for (int i = 0; i < val.Length; i = i + 2) + { + string hexChars = val.Substring(i, 2); + ret[i / 2] = byte.Parse(hexChars, System.Globalization.NumberStyles.HexNumber); + } + return ret; + } + + public static Address HexToAddress(this string hexString) + { + // uint160 only parses a big-endian hex string + var result = HexStringToBytes(hexString); + return CreateAddress(result); + } + + private static Address CreateAddress(byte[] bytes) + { + uint pn0 = BitConverter.ToUInt32(bytes, 0); + uint pn1 = BitConverter.ToUInt32(bytes, 4); + uint pn2 = BitConverter.ToUInt32(bytes, 8); + uint pn3 = BitConverter.ToUInt32(bytes, 12); + uint pn4 = BitConverter.ToUInt32(bytes, 16); + + return new Address(pn0, pn1, pn2, pn3, pn4); + } + } +} diff --git a/Mainnet/DividendToken/DividendToken.Tests/DividendToken.Tests.csproj b/Mainnet/DividendToken/DividendToken.Tests/DividendToken.Tests.csproj new file mode 100644 index 00000000..ffc22b61 --- /dev/null +++ b/Mainnet/DividendToken/DividendToken.Tests/DividendToken.Tests.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp3.1 + DividendTokenContract.Tests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Mainnet/DividendToken/DividendToken.Tests/DividendTokenTests.cs b/Mainnet/DividendToken/DividendToken.Tests/DividendTokenTests.cs new file mode 100644 index 00000000..f56035e6 --- /dev/null +++ b/Mainnet/DividendToken/DividendToken.Tests/DividendTokenTests.cs @@ -0,0 +1,212 @@ +using Moq; +using Stratis.SmartContracts; +using Xunit; + +namespace DividendTokenContract.Tests +{ + public class DividendTokenTests + { + private readonly IPersistentState state; + + private readonly Mock mContractState; + private readonly Mock mContractLogger; + private readonly Mock mTransactionExecutor; + + private readonly Address owner; + private readonly Address tokenHolder; + private readonly Address currentContract; + + private readonly string name; + private readonly string symbol; + private readonly UInt256 totalSupply; + private readonly byte decimals; + + public DividendTokenTests() + { + this.state = new InMemoryState(); + this.mContractState = new Mock(); + this.mContractLogger = new Mock(); + this.mTransactionExecutor = new Mock(); + this.mContractState.Setup(s => s.PersistentState).Returns(state); + this.mContractState.Setup(s => s.ContractLogger).Returns(mContractLogger.Object); + this.mContractState.Setup(s => s.InternalTransactionExecutor).Returns(mTransactionExecutor.Object); + this.owner = "0x0000000000000000000000000000000000000001".HexToAddress(); + this.tokenHolder = "0x0000000000000000000000000000000000000002".HexToAddress(); + this.currentContract = "0x0000000000000000000000000000000000000003".HexToAddress(); + this.name = "Test Token"; + this.symbol = "TST"; + this.totalSupply = 1_000; + this.decimals = 0; + } + + + [Fact] + public void Deposited_Dividend_Should_Be_Distributed_Equaly() + { + var dividend = 1000ul; + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, owner, dividend)); + + var contract = new DividendToken(mContractState.Object, totalSupply, name, symbol, decimals); + + Assert.True(contract.TransferTo(tokenHolder, 100)); + + contract.Receive(); + + Assert.Equal(dividend, contract.Dividends); + Assert.Equal(100ul, contract.GetDividends(tokenHolder)); + Assert.Equal(100ul, contract.GetTotalDividends(tokenHolder)); + Assert.Equal(900ul, contract.GetDividends(owner)); + Assert.Equal(900ul, contract.GetTotalDividends(owner)); + } + + [Fact] + public void Multiple_Deposited_Dividend_Should_Be_Distributed_Equaly() + { + var dividend = 1000ul; + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, owner, dividend)); + + var contract = new DividendToken(mContractState.Object, totalSupply, name, symbol, decimals); + + Assert.True(contract.TransferTo(tokenHolder, 100)); + + contract.Receive(); + + Assert.True(contract.TransferTo(tokenHolder, 100)); + + contract.Receive(); + + Assert.True(contract.TransferTo(tokenHolder, 100)); + + Assert.Equal(2 * dividend, contract.Dividends); + Assert.Equal(300ul, contract.GetDividends(tokenHolder)); + Assert.Equal(300ul, contract.GetTotalDividends(tokenHolder)); + + Assert.Equal(1700ul, contract.GetDividends(owner)); + Assert.Equal(1700ul, contract.GetTotalDividends(owner)); + } + + [Fact] + public void Cumulative_Dividends_Should_Be_Distributed_Equaly() + { + var dividend = 1000ul; + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, owner, dividend)); + + var contract = new DividendToken(mContractState.Object, totalSupply, name, symbol, decimals); + + Assert.True(contract.TransferTo(tokenHolder, 100)); + + contract.Receive(); + contract.Receive(); + + Assert.Equal(2 * dividend, contract.Dividends); + Assert.Equal(2 * 100ul, contract.GetDividends(tokenHolder)); + Assert.Equal(2 * 900ul, contract.GetDividends(owner)); + } + + /// + /// In the case of dividend per token is 1.5 satoshi, + /// address that holds 1 token will have 1 dividend at first deposit despite actual earned amount is 1.5 satoshi + /// because satoshi unit doesn't support decimal points. Still 0.5 satoshi is accounted in the account + /// But after second deposit by same rate (0.5 satoshi per token), the user will be able to have 1 satoshi (0.5 + 0.5) + /// + [Fact] + public void Cumulative_Deposits_Should_Be_Distributed_Equaly_When_Dividends_Have_Decimal_Value() + { + var dividend = 500ul; + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, owner, dividend)); + + var contract = new DividendToken(mContractState.Object, totalSupply, name, symbol, decimals); + + Assert.True(contract.TransferTo(tokenHolder, 1)); + + contract.Receive(); + + Assert.Equal(dividend, contract.Dividends); + Assert.Equal(0ul, contract.GetDividends(tokenHolder)); + Assert.Equal(499ul, contract.GetDividends(owner)); + + contract.Receive(); + + Assert.Equal(2 * dividend, contract.Dividends); + Assert.Equal(1ul, contract.GetDividends(tokenHolder)); + Assert.Equal(999ul, contract.GetDividends(owner)); + } + + [Fact] + public void Deposited_Dividend_Should_Be_Withdrawable() + { + var dividend = 500ul; + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, owner, dividend)); + mContractState.Setup(m => m.GetBalance).Returns(() => dividend); + mTransactionExecutor.Setup(m => m.Transfer(mContractState.Object, tokenHolder, 5)).Returns(TransferResult.Succeed(true)); + + var contract = new DividendToken(mContractState.Object, totalSupply, name, symbol, decimals); + + Assert.True(contract.TransferTo(tokenHolder, 11)); + + contract.Receive(); + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, tokenHolder, 0)); + + contract.Withdraw(); + + mTransactionExecutor.Verify(s => s.Transfer(mContractState.Object, tokenHolder, 5), Times.Once); + Assert.Equal(0ul, contract.GetDividends()); + var account = state.GetStruct($"Account:{tokenHolder}"); + Assert.Equal((UInt256)500, account.DividendBalance); + Assert.Equal(5ul, account.WithdrawnDividends); + Assert.Equal(dividend, account.CreditedDividends); + } + + [Fact] + public void GetDividends_Returns_Current_Sender_Dividends() + { + var dividend = 1000ul; + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, owner, dividend)); + mContractState.Setup(m => m.GetBalance).Returns(() => dividend); + mTransactionExecutor.Setup(m => m.Transfer(mContractState.Object, tokenHolder, 100)).Returns(TransferResult.Succeed(true)); + + var contract = new DividendToken(mContractState.Object, totalSupply, name, symbol, decimals); + + Assert.True(contract.TransferTo(tokenHolder, 100)); + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, tokenHolder, dividend)); + contract.Receive(); + + Assert.Equal(100ul, contract.GetDividends()); + } + + /// + /// GetTotalDividends should to return Withdrawable + Withdrawn dividends + /// + [Fact] + public void GetTotalDividends_Returns_Current_Sender_TotalDividends() + { + var dividend = 1000ul; + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, owner, dividend)); + mContractState.Setup(m => m.GetBalance).Returns(() => dividend); + mTransactionExecutor.Setup(m => m.Transfer(mContractState.Object, tokenHolder, 100)).Returns(TransferResult.Succeed(true)); + + var contract = new DividendToken(mContractState.Object, totalSupply, name, symbol, decimals); + + Assert.True(contract.TransferTo(tokenHolder, 100)); + + contract.Receive(); + + mContractState.Setup(m => m.Message).Returns(new Message(currentContract, tokenHolder, 0)); + + contract.Withdraw(); + + mTransactionExecutor.Verify(s => s.Transfer(mContractState.Object, tokenHolder, 100), Times.Once); + Assert.Equal(0ul, contract.GetDividends()); + Assert.Equal(100ul, contract.GetTotalDividends()); + } + } +} diff --git a/Mainnet/DividendToken/DividendToken.Tests/InMemoryState.cs b/Mainnet/DividendToken/DividendToken.Tests/InMemoryState.cs new file mode 100644 index 00000000..53e14132 --- /dev/null +++ b/Mainnet/DividendToken/DividendToken.Tests/InMemoryState.cs @@ -0,0 +1,83 @@ +using Stratis.SmartContracts; +using System; +using System.Collections.Generic; + +namespace DividendTokenContract.Tests +{ + public class InMemoryState : IPersistentState + { + private readonly Dictionary storage = new Dictionary(); + public bool IsContractResult { get; set; } + public void Clear(string key) => storage.Remove(key); + + public T GetValue(string key) => (T)storage.GetValueOrDefault(key, default(T)); + + public void AddOrReplace(string key, object value) + { + if (!storage.TryAdd(key, value)) + storage[key] = value; + } + public Address GetAddress(string key) => GetValue
(key); + + public T[] GetArray(string key) => GetValue(key); + + public bool GetBool(string key) => GetValue(key); + + public byte[] GetBytes(byte[] key) => throw new NotImplementedException(); + + public byte[] GetBytes(string key) => GetValue(key); + + public char GetChar(string key) => GetValue(key); + + public int GetInt32(string key) => GetValue(key); + + public long GetInt64(string key) => GetValue(key); + + public string GetString(string key) => GetValue(key); + + public T GetStruct(string key) + where T : struct => GetValue(key); + + public uint GetUInt32(string key) => GetValue(key); + + public ulong GetUInt64(string key) => GetValue(key); + + public UInt128 GetUInt128(string key) => GetValue(key); + + public UInt256 GetUInt256(string key) => GetValue(key); + + public bool IsContract(Address address) => IsContractResult; + + public void SetAddress(string key, Address value) => AddOrReplace(key, value); + + public void SetArray(string key, Array a) => AddOrReplace(key, a); + + public void SetBool(string key, bool value) => AddOrReplace(key, value); + + public void SetBytes(byte[] key, byte[] value) + { + throw new NotImplementedException(); + } + + public void SetBytes(string key, byte[] value) => AddOrReplace(key, value); + + public void SetChar(string key, char value) => AddOrReplace(key, value); + + public void SetInt32(string key, int value) => AddOrReplace(key, value); + + public void SetInt64(string key, long value) => AddOrReplace(key, value); + + public void SetString(string key, string value) => AddOrReplace(key, value); + + public void SetStruct(string key, T value) + where T : struct => AddOrReplace(key, value); + + public void SetUInt32(string key, uint value) => AddOrReplace(key, value); + + public void SetUInt64(string key, ulong value) => AddOrReplace(key, value); + + public void SetUInt128(string key, UInt128 value) => AddOrReplace(key, value); + + public void SetUInt256(string key, UInt256 value) => AddOrReplace(key, value); + } +} \ No newline at end of file diff --git a/Mainnet/DividendToken/DividendToken.Tests/TransferResult.cs b/Mainnet/DividendToken/DividendToken.Tests/TransferResult.cs new file mode 100644 index 00000000..a20cd46f --- /dev/null +++ b/Mainnet/DividendToken/DividendToken.Tests/TransferResult.cs @@ -0,0 +1,19 @@ +using Stratis.SmartContracts; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DividendTokenContract.Tests +{ + public class TransferResult : ITransferResult + { + public object ReturnValue { get; private set; } + + public bool Success { get; private set; } + + public static TransferResult Failed() => new TransferResult { Success = false }; + + + public static TransferResult Succeed(object returnValue) => new TransferResult { Success = true, ReturnValue = returnValue }; + } +} diff --git a/Mainnet/DividendToken/DividendToken.sln b/Mainnet/DividendToken/DividendToken.sln new file mode 100644 index 00000000..fff0a409 --- /dev/null +++ b/Mainnet/DividendToken/DividendToken.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30204.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DividendToken", "DividendToken\DividendToken.csproj", "{3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DividendToken.Tests", "DividendToken.Tests\DividendToken.Tests.csproj", "{3B5AF108-6986-4E17-9A01-EB297D6F0C15}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Release|Any CPU.Build.0 = Release|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA15305F-3348-4660-BF5B-3D0CD51F0FFD} + EndGlobalSection +EndGlobal diff --git a/Mainnet/DividendToken/DividendToken/DividendToken.cs b/Mainnet/DividendToken/DividendToken/DividendToken.cs new file mode 100644 index 00000000..749760a8 --- /dev/null +++ b/Mainnet/DividendToken/DividendToken/DividendToken.cs @@ -0,0 +1,305 @@ +using Stratis.SmartContracts; +using Stratis.SmartContracts.Standards; + +[Deploy] +public class DividendToken : SmartContract, IStandardToken256 +{ + public ulong Dividends + { + get => State.GetUInt64(nameof(this.Dividends)); + private set => State.SetUInt64(nameof(this.Dividends), value); + } + + private Account GetAccount(Address address) => State.GetStruct($"Account:{address}"); + + private void SetAccount(Address address, Account account) => State.SetStruct($"Account:{address}", account); + + + public DividendToken(ISmartContractState state, UInt256 totalSupply, string name, string symbol, byte decimals) + : base(state) + { + this.TotalSupply = totalSupply; + this.Name = name; + this.Symbol = symbol; + this.Decimals = decimals; + this.SetBalance(Message.Sender, totalSupply); + } + + + public override void Receive() + { + DistributeDividends(); + } + + /// + /// It is advised that deposit amount should to be evenly divided by total supply, + /// otherwise small amount of satoshi may lost(burn) + /// + public void DistributeDividends() + { + Dividends += Message.Value; + } + + public bool TransferTo(Address to, UInt256 amount) + { + UpdateAccount(Message.Sender); + UpdateAccount(to); + + return TransferTokensTo(to, amount); + } + + public bool TransferFrom(Address from, Address to, UInt256 amount) + { + UpdateAccount(from); + UpdateAccount(to); + + return TransferTokensFrom(from, to, amount); + } + + private Account UpdateAccount(Address address) + { + var account = GetAccount(address); + var newDividends = GetNewDividends(address, account); + + if (newDividends > 0) + { + account.DividendBalance += newDividends; + account.CreditedDividends = Dividends; + SetAccount(address, account); + } + + return account; + } + + private UInt256 GetWithdrawableDividends(Address address, Account account) + { + return account.DividendBalance + GetNewDividends(address, account); //Delay divide by TotalSupply to final stage for avoid decimal value loss. + } + + private UInt256 GetNewDividends(Address address, Account account) + { + var notCreditedDividends = checked(Dividends - account.CreditedDividends); + return GetBalance(address) * notCreditedDividends; + } + + /// + /// Get Withdrawable dividends + /// + /// + public ulong GetDividends() => GetDividends(Message.Sender); + + /// + /// Get Withdrawable dividends + /// + /// + /// + public ulong GetDividends(Address address) + { + var account = GetAccount(address); + + var withdrawable = GetWithdrawableDividends(address, account) / TotalSupply; + + return (ulong)withdrawable; + } + + /// + /// Get the all divididends since beginning (Withdrawable Dividends + Withdrawn Dividends) + /// + /// + public ulong GetTotalDividends() => GetTotalDividends(Message.Sender); + + /// + /// Get the all divididends since beginning (Withdrawable Dividends + Withdrawn Dividends) + /// + /// + /// + public ulong GetTotalDividends(Address address) + { + var account = GetAccount(address); + var withdrawable = GetWithdrawableDividends(address, account) / TotalSupply; + return (ulong)withdrawable + account.WithdrawnDividends; + } + + /// + /// Withdraws all dividends + /// + public void Withdraw() + { + var account = UpdateAccount(Message.Sender); + var balance = (ulong)(account.DividendBalance / TotalSupply); + + Assert(balance > 0, "The account has no dividends."); + + account.WithdrawnDividends += balance; + + account.DividendBalance %= TotalSupply; + + SetAccount(Message.Sender, account); + + var transfer = Transfer(Message.Sender, balance); + + Assert(transfer.Success, "Transfer failed."); + } + + public struct Account + { + /// + /// Withdrawable Dividend Balance. Exact value should to divided by + /// + public UInt256 DividendBalance; + + public ulong WithdrawnDividends; + + /// + /// Dividends computed and added to + /// + + public ulong CreditedDividends; + } + + #region StandardToken code is inlined + + + public string Symbol + { + get => State.GetString(nameof(this.Symbol)); + private set => State.SetString(nameof(this.Symbol), value); + } + + public string Name + { + get => State.GetString(nameof(this.Name)); + private set => State.SetString(nameof(this.Name), value); + } + + /// + public UInt256 TotalSupply + { + get => State.GetUInt256(nameof(this.TotalSupply)); + private set => State.SetUInt256(nameof(this.TotalSupply), value); + } + + public byte Decimals + { + get => State.GetBytes(nameof(Decimals))[0]; + private set => State.SetBytes(nameof(Decimals), new[] { value }); + } + + + /// + public UInt256 GetBalance(Address address) + { + return State.GetUInt256($"Balance:{address}"); + } + + private void SetBalance(Address address, UInt256 value) + { + State.SetUInt256($"Balance:{address}", value); + } + + /// + private bool TransferTokensTo(Address to, UInt256 amount) + { + if (amount == 0) + { + Log(new TransferLog { From = Message.Sender, To = to, Amount = 0 }); + + return true; + } + + UInt256 senderBalance = GetBalance(Message.Sender); + + if (senderBalance < amount) + { + return false; + } + + SetBalance(Message.Sender, senderBalance - amount); + + SetBalance(to, checked(GetBalance(to) + amount)); + + Log(new TransferLog { From = Message.Sender, To = to, Amount = amount }); + + return true; + } + + /// + private bool TransferTokensFrom(Address from, Address to, UInt256 amount) + { + if (amount == 0) + { + Log(new TransferLog { From = from, To = to, Amount = 0 }); + + return true; + } + + UInt256 senderAllowance = Allowance(from, Message.Sender); + UInt256 fromBalance = GetBalance(from); + + if (senderAllowance < amount || fromBalance < amount) + { + return false; + } + + SetApproval(from, Message.Sender, senderAllowance - amount); + + SetBalance(from, fromBalance - amount); + + SetBalance(to, checked(GetBalance(to) + amount)); + + Log(new TransferLog { From = from, To = to, Amount = amount }); + + return true; + } + + /// + public bool Approve(Address spender, UInt256 currentAmount, UInt256 amount) + { + if (Allowance(Message.Sender, spender) != currentAmount) + { + return false; + } + + SetApproval(Message.Sender, spender, amount); + + Log(new ApprovalLog { Owner = Message.Sender, Spender = spender, Amount = amount, OldAmount = currentAmount }); + + return true; + } + + private void SetApproval(Address owner, Address spender, UInt256 value) + { + State.SetUInt256($"Allowance:{owner}:{spender}", value); + } + + /// + public UInt256 Allowance(Address owner, Address spender) + { + return State.GetUInt256($"Allowance:{owner}:{spender}"); + } + + public struct TransferLog + { + [Index] + public Address From; + + [Index] + public Address To; + + public UInt256 Amount; + } + + public struct ApprovalLog + { + [Index] + public Address Owner; + + [Index] + public Address Spender; + + public UInt256 OldAmount; + + public UInt256 Amount; + } + #endregion +} \ No newline at end of file diff --git a/Mainnet/DividendToken/DividendToken/DividendToken.csproj b/Mainnet/DividendToken/DividendToken/DividendToken.csproj new file mode 100644 index 00000000..8733aff9 --- /dev/null +++ b/Mainnet/DividendToken/DividendToken/DividendToken.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.1 + 8.0 + + + + + + diff --git a/Mainnet/DividendToken/README.MD b/Mainnet/DividendToken/README.MD new file mode 100644 index 00000000..db698d8e --- /dev/null +++ b/Mainnet/DividendToken/README.MD @@ -0,0 +1,11 @@ +# Dividend Token Contract + +**Contract Hash** +``` +41a37d258e324a6c51ec2099a1a20546c696fecf2a649bd6e6cd8add835d46ce +``` + +**Contract Byte Code** +``` +4D5A90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000800000000E1FBA0E00B409CD21B8014CCD21546869732070726F6772616D2063616E6E6F742062652072756E20696E20444F53206D6F64652E0D0D0A2400000000000000504500004C0102001B3479CF0000000000000000E00022200B013000001400000002000000000000B2320000002000000040000000000010002000000002000004000000000000000400000000000000006000000002000000000000030040850000100000100000000010000010000000000000100000000000000000000000603200004F000000000000000000000000000000000000000000000000000000004000000C000000443200001C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000082000004800000000000000000000002E74657874000000B8120000002000000014000000020000000000000000000000000000200000602E72656C6F6300000C00000000400000000200000016000000000000000000000000000040000042000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000094320000000000004800000002000500AC250000980C000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004602280600000A72010000706F0700000A2A4A02280600000A7201000070036F0800000A2A7202280600000A7215000070038C08000001280900000A6F0100002B2A7602280600000A7215000070038C08000001280900000A046F0200002B2AC20203280C00000A0204281700000602052815000006020E0428130000060202280D00000A6F0E00000A0428190000062A1E0228070000062A660202280100000602280D00000A6F0F00000AD728020000062A8E0202280D00000A6F0E00000A280A000006260203280A00000626020304281A0000062A6A0203280A000006260204280A0000062602030405281B0000062A0000133003003C00000001000011020328030000060A020306280C0000060B07166A36241200067B0100000407D77D0100000412000228010000067D030000040203062804000006062A42047B01000004020304280C000006D72A000000133002001800000002000011022801000006047B03000004DB0A0203281800000606D92A4A0202280D00000A6F0E00000A280E0000062A00133003001800000003000011020328030000060A020306280B0000060228160000065C2A4A0202280D00000A6F0E00000A28100000062A00133003001F00000003000011020328030000060A020306280B000006067B02000004D70228160000065C2A001330030093000000040000110202280D00000A6F0E00000A280A0000060A067B010000040228160000065C0B067B010000040228160000065E0C0207166AFE03722D000070281000000A1200067B02000004067B01000004D708DB7D020000041200087D010000040202280D00000A6F0E00000A0628040000060202280D00000A6F0E00000A07281100000A0D02096F1200000A7269000070281000000A2A4602280600000A728B0000706F1300000A2A4A02280600000A728B000070036F1400000A2A4602280600000A72990000706F1300000A2A4A02280600000A7299000070036F1400000A2A4602280600000A72A30000706F0700000A2A4A02280600000A72A3000070036F0800000A2A7202280600000A72BB000070038C08000001280900000A6F0700000A2A7602280600000A72BB000070038C08000001280900000A046F0800000A2A00000013300400A600000005000011042D34021201FE1504000002120102280D00000A6F0E00000A7D040000041201037D050000041201166A7D0600000407280300002B172A0202280D00000A6F0E00000A28180000060A06043402162A0202280D00000A6F0E00000A0604DB281900000602030203281800000604D72819000006021201FE1504000002120102280D00000A6F0E00000A7D040000041201037D050000041201047D0600000407280300002B172A000013300500AA00000006000011052D2A021202FE15040000021202037D040000041202047D050000041202166A7D0600000408280300002B172A020302280D00000A6F0E00000A281E0000060A020328180000060B0605370407053402162A020302280D00000A6F0E00000A0605DB281D00000602030705DB281900000602040204281800000605D72819000006021202FE15040000021202037D040000041202047D050000041202057D0600000408280300002B172A00001330040065000000070000110202280D00000A6F0E00000A03281E000006042E02162A0202280D00000A6F0E00000A0305281D000006021200FE1505000002120002280D00000A6F0E00000A7D070000041200037D080000041200057D0A0000041200047D0900000406280400002B172A8E02280600000A72D3000070038C08000001048C08000001281600000A056F0800000A2A8A02280600000A72D3000070038C08000001048C08000001281600000A6F0700000A2A42534A4201000100000000000C00000076342E302E33303331390000000005006C00000058050000237E0000C40500006C04000023537472696E677300000000300A0000F800000023555300280B0000100000002347554944000000380B00006001000023426C6F6200000000000000020000015717A201090A000000FA0133001600000100000010000000050000000A0000001E0000002700000001000000160000000800000007000000010000000400000008000000010000000300000003000000040000000000A3010100000000000600EE00410306002D0141030600DA0070020F00610300000A001D018C030A00AA038C030E000C0220030A007C038C030A009B008C030A00D3038C0306009100D2010A000E018C030A00AF008C0306007801D2010600B803D2010A0065008C03000000001500000000000100010001001000FE0100001900010001000A011000FF0300002D0001001F000A0110008B0100002D0004001F000A0110007F0100002D0007001F0006003500B6000600F002B60006009F02B6000600F401B90006003402B90006002004B60006005E02B90006004502B90006000F04B60006002004B600502000000000860883024A00010062200000000081089102BD0001007520000000008100F103C20002009220000000008100FC03C9000300B0200000000086186A02D1000500E12000000000C6005B0106000900E920000000008600CA0206000900032100000000E6011B02DA000900272100000000E601D901E1000B004421000000008100E303C2000E008C21000000008100B102EA000F00A0210000000081001003EA001100C42100000000860003034A001300D8210000000086000303F2001300FC21000000008600DE024A0014001022000000008600DE02F20014003C22000000008600350406001500DB22000000008608B501F8001500ED22000000008108C001FC00150000230000000086087A00F800160012230000000081088300FC001600252300000000E6093E044A00170037230000000081084E04BD0017004A2300000000E6014500F2001800672300000000810050000101190088230000000081002602DA001B003C24000000008100E601E1001D00F42400000000E6016301080120006525000000008100970110012300892500000000E6015B001901260000000100550100000100840300000100840300000200070400000100D400000002005E04000003008C0000000400CB0100000100370200000200270400000100F90100000200370200000300270400000100840300000100840300000200070400000100840300000200070400000100840300000100840300000100550100000100550100000100550100000100840300000100840300000200550100000100370200000200270400000100F901000002003702000003002704000001004D02000002001904000003002704000001006402000002004D02000003005501000001006402000002004D0202001D0009006A02010011006A02060019006A020A0029006A02060061006A0206003100C000100069000100150069000B001A007100A30320006900BF0326006900C903320031006A023A0031006E00400081003A02450081004B014A0031002E046600310055026C0051007003740069006B017800690075017D003100930189007100A303A6002E000B0029012E00130032012E001B005101430023005A0181002B005A01A1002B005A01E1002B005A0101012B005A014E00540058005D00830095009C00020001000000160321010000C4012501000087002501000052042101020001000300010002000300020012000500010013000500020014000700010015000700020016000900010017000900048000000000000000000000000000000000AA030000040000000000000000000000AD001E000000000001000200010000000000000000008C0300000000010000000000000000000000000020030000000003000200040002000500020015002D0017002D002B0090002B00A10000000047657455496E7436340053657455496E743634003C4D6F64756C653E0053797374656D2E507269766174652E436F72654C6962004469766964656E6442616C616E63650047657442616C616E63650053657442616C616E636500416C6C6F77616E636500494D657373616765006765745F4D657373616765006765745F4E616D65007365745F4E616D65006E616D650056616C7565547970650049536D617274436F6E74726163745374617465004950657273697374656E745374617465006765745F50657273697374656E7453746174650073746174650044656275676761626C6541747472696275746500436F6D70696C6174696F6E52656C61786174696F6E7341747472696275746500496E646578417474726962757465004465706C6F794174747269627574650052756E74696D65436F6D7061746962696C697479417474726962757465006765745F56616C75650076616C7565005265636569766500417070726F766500476574537472696E6700536574537472696E6700417070726F76616C4C6F67005472616E736665724C6F6700536574417070726F76616C00536D617274436F6E74726163742E646C6C006765745F53796D626F6C007365745F53796D626F6C0073796D626F6C0053797374656D005472616E7366657246726F6D005472616E73666572546F6B656E7346726F6D0066726F6D004469766964656E64546F6B656E00495374616E64617264546F6B656E005472616E73666572546F005472616E73666572546F6B656E73546F00746F006765745F53656E646572005370656E646572007370656E646572005472616E73666572004F776E6572006F776E6572002E63746F720053797374656D2E446961676E6F7374696373006765745F4469766964656E6473007365745F4469766964656E64730043726564697465644469766964656E647300476574576974686472617761626C654469766964656E647300446973747269627574654469766964656E647300476574546F74616C4469766964656E64730057697468647261776E4469766964656E6473004765744469766964656E6473004765744E65774469766964656E647300537472617469732E536D617274436F6E7472616374732E5374616E64617264730053797374656D2E52756E74696D652E436F6D70696C6572536572766963657300446562756767696E674D6F646573006765745F537563636573730041646472657373006164647265737300537472617469732E536D617274436F6E74726163747300466F726D617400536D617274436F6E7472616374004F626A656374004765745374727563740053657453747275637400495472616E73666572526573756C74005570646174654163636F756E74004765744163636F756E74005365744163636F756E74006163636F756E74004F6C64416D6F756E740063757272656E74416D6F756E7400616D6F756E7400417373657274005769746864726177006765745F546F74616C537570706C79007365745F546F74616C537570706C7900746F74616C537570706C7900000000134400690076006900640065006E006400730000174100630063006F0075006E0074003A007B0030007D00003B54006800650020006100630063006F0075006E007400200068006100730020006E006F0020006400690076006900640065006E00640073002E0000215400720061006E00730066006500720020006600610069006C00650064002E00000D530079006D0062006F006C0000094E0061006D006500001754006F00740061006C0053007500700070006C0079000017420061006C0061006E00630065003A007B0030007D00002341006C006C006F00770061006E00630065003A007B0030007D003A007B0031007D00000043222F817C95CB4498A926937E6400A20004200101080320000105200101111104200012350420010B0E052002010E0B0500020E0E1C063001011E000E040A01110C07300102010E1E00052001011225042000124104200011210320000B050702110C0B0307010B040701110C080704110C0B0B122905200201020E072002122911210B032000020420010E0E052002010E0E0507020B111006300101011E00040A0111100607030B0B11100407011114040A0111140600030E0E1C1C087CEC85D7BEA7798E02060B03061121042001010B062001110C1121072002011121110C0820040112250B0E0E0620020211210B08200302112111210B0720020B1121110C0520010B11210320000E042001010E0620020111210B0720030211210B0B08200301112111210B0720020B112111210328000B0328000E0801000800000000001E01000100540216577261704E6F6E457863657074696F6E5468726F77730108010002000000000004010000000000000000000000000000000010000000000000000000000000000000883200000000000000000000A232000000200000000000000000000000000000000000000000000094320000000000000000000000005F436F72446C6C4D61696E006D73636F7265652E646C6C0000000000FF250020001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000C000000B43200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +``` diff --git a/Testnet/DividendToken/DividendToken.Tests/DividendToken.Tests.csproj b/Testnet/DividendToken/DividendToken.Tests/DividendToken.Tests.csproj new file mode 100644 index 00000000..be593740 --- /dev/null +++ b/Testnet/DividendToken/DividendToken.Tests/DividendToken.Tests.csproj @@ -0,0 +1,22 @@ + + + + Exe + netcoreapp3.1 + DividendTokenContract.Tests + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Testnet/DividendToken/DividendToken.Tests/DividendTokenTests.cs b/Testnet/DividendToken/DividendToken.Tests/DividendTokenTests.cs new file mode 100644 index 00000000..98b908a2 --- /dev/null +++ b/Testnet/DividendToken/DividendToken.Tests/DividendTokenTests.cs @@ -0,0 +1,212 @@ +using Moq; +using Stratis.SmartContracts; +using Stratis.SmartContracts.CLR; +using Xunit; + +namespace DividendTokenContract.Tests +{ + public class DividendTokenTests + { + private readonly IPersistentState persistentState; + + private readonly Mock mContractState; + private readonly Mock mContractLogger; + private readonly Mock mTransactionExecutor; + + private readonly Address owner; + private readonly Address tokenHolder; + private readonly Address contract; + + private readonly string name; + private readonly string symbol; + private readonly UInt256 totalSupply; + private readonly uint decimals; + + public DividendTokenTests() + { + this.persistentState = new InMemoryState(); + this.mContractState = new Mock(); + this.mContractLogger = new Mock(); + this.mTransactionExecutor = new Mock(); + this.mContractState.Setup(s => s.PersistentState).Returns(this.persistentState); + this.mContractState.Setup(s => s.ContractLogger).Returns(this.mContractLogger.Object); + this.mContractState.Setup(s => s.InternalTransactionExecutor).Returns(this.mTransactionExecutor.Object); + this.owner = "0x0000000000000000000000000000000000000001".HexToAddress(); + this.tokenHolder = "0x0000000000000000000000000000000000000002".HexToAddress(); + this.contract = "0x0000000000000000000000000000000000000003".HexToAddress(); + this.name = "Test Token"; + this.symbol = "TST"; + this.totalSupply = 1_000; + this.decimals = 0; + } + + [Fact] + public void Deposited_Dividend_Should_Be_Distributed_Equaly() + { + var dividend = 1000ul; + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.owner, dividend)); + + var contract = new DividendToken(this.mContractState.Object, this.totalSupply, this.name, this.symbol, this.decimals); + + Assert.True(contract.TransferTo(this.tokenHolder, 100)); + + contract.Receive(); + + Assert.Equal(dividend, contract.Dividends); + Assert.Equal(100ul, contract.GetDividends(this.tokenHolder)); + Assert.Equal(100ul, contract.GetTotalDividends(this.tokenHolder)); + Assert.Equal(900ul, contract.GetDividends(this.owner)); + Assert.Equal(900ul, contract.GetTotalDividends(this.owner)); + } + + [Fact] + public void Multiple_Deposited_Dividend_Should_Be_Distributed_Equaly() + { + var dividend = 1000ul; + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.owner, dividend)); + + var contract = new DividendToken(this.mContractState.Object, this.totalSupply, this.name, this.symbol, this.decimals); + + Assert.True(contract.TransferTo(this.tokenHolder, 100)); + + contract.Receive(); + + Assert.True(contract.TransferTo(this.tokenHolder, 100)); + + contract.Receive(); + + Assert.True(contract.TransferTo(this.tokenHolder, 100)); + + Assert.Equal(2 * dividend, contract.Dividends); + Assert.Equal(300ul, contract.GetDividends(this.tokenHolder)); + Assert.Equal(300ul, contract.GetTotalDividends(this.tokenHolder)); + + Assert.Equal(1700ul, contract.GetDividends(this.owner)); + Assert.Equal(1700ul, contract.GetTotalDividends(this.owner)); + } + + [Fact] + public void Cumulative_Dividends_Should_Be_Distributed_Equaly() + { + var dividend = 1000ul; + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.owner, dividend)); + + var contract = new DividendToken(this.mContractState.Object, this.totalSupply, this.name, this.symbol, this.decimals); + + Assert.True(contract.TransferTo(this.tokenHolder, 100)); + + contract.Receive(); + contract.Receive(); + + Assert.Equal(2 * dividend, contract.Dividends); + Assert.Equal(2 * 100ul, contract.GetDividends(this.tokenHolder)); + Assert.Equal(2 * 900ul, contract.GetDividends(this.owner)); + } + + /// + /// In the case of dividend per token is 1.5 satoshi, + /// address that holds 1 token will have 1 dividend at first deposit despite actual earned amount is 1.5 satoshi + /// because satoshi unit doesn't support decimal points. Still 0.5 satoshi is accounted in the account + /// But after second deposit by same rate (0.5 satoshi per token), the user will be able to have 1 satoshi (0.5 + 0.5) + /// + [Fact] + public void Cumulative_Deposits_Should_Be_Distributed_Equaly_When_Dividends_Have_Decimal_Value() + { + var dividend = 500ul; + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.owner, dividend)); + + var contract = new DividendToken(this.mContractState.Object, this.totalSupply, this.name, this.symbol, this.decimals); + + Assert.True(contract.TransferTo(this.tokenHolder, 1)); + + contract.Receive(); + + Assert.Equal(dividend, contract.Dividends); + Assert.Equal(0ul, contract.GetDividends(this.tokenHolder)); + Assert.Equal(499ul, contract.GetDividends(this.owner)); + + contract.Receive(); + + Assert.Equal(2 * dividend, contract.Dividends); + Assert.Equal(1ul, contract.GetDividends(this.tokenHolder)); + Assert.Equal(999ul, contract.GetDividends(this.owner)); + } + + [Fact] + public void Deposited_Dividend_Should_Be_Withdrawable() + { + var dividend = 500ul; + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.owner, dividend)); + this.mContractState.Setup(m => m.GetBalance).Returns(() => dividend); + this.mTransactionExecutor.Setup(m => m.Transfer(this.mContractState.Object, this.tokenHolder, 5)).Returns(TransferResult.Transferred(true)); + + var contract = new DividendToken(this.mContractState.Object, this.totalSupply, this.name, this.symbol, this.decimals); + + Assert.True(contract.TransferTo(this.tokenHolder, 11)); + + contract.Receive(); + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.tokenHolder, 0)); + + contract.Withdraw(); + + this.mTransactionExecutor.Verify(s => s.Transfer(this.mContractState.Object, this.tokenHolder, 5), Times.Once); + Assert.Equal(0ul, contract.GetDividends()); + var account = this.persistentState.GetStruct($"Account:{this.tokenHolder}"); + Assert.Equal((UInt256)500, account.DividendBalance); + Assert.Equal(5ul, account.WithdrawnDividends); + Assert.Equal(dividend, account.CreditedDividends); + } + + [Fact] + public void GetDividends_Returns_Current_Sender_Dividends() + { + var dividend = 1000ul; + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.owner, dividend)); + this.mContractState.Setup(m => m.GetBalance).Returns(() => dividend); + this.mTransactionExecutor.Setup(m => m.Transfer(this.mContractState.Object, this.tokenHolder, 100)).Returns(TransferResult.Transferred(true)); + + var contract = new DividendToken(this.mContractState.Object, this.totalSupply, this.name, this.symbol, this.decimals); + + Assert.True(contract.TransferTo(this.tokenHolder, 100)); + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.tokenHolder, dividend)); + contract.Receive(); + + Assert.Equal(100ul, contract.GetDividends()); + } + + /// + /// GetTotalDividends should to return Withdrawable + Withdrawn dividends + /// + [Fact] + public void GetTotalDividends_Returns_Current_Sender_TotalDividends() + { + var dividend = 1000ul; + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.owner, dividend)); + this.mContractState.Setup(m => m.GetBalance).Returns(() => dividend); + this.mTransactionExecutor.Setup(m => m.Transfer(this.mContractState.Object, this.tokenHolder, 100)).Returns(TransferResult.Transferred(true)); + + var contract = new DividendToken(this.mContractState.Object, this.totalSupply, this.name, this.symbol, this.decimals); + + Assert.True(contract.TransferTo(this.tokenHolder, 100)); + + contract.Receive(); + + this.mContractState.Setup(m => m.Message).Returns(new Message(this.contract, this.tokenHolder, 0)); + + contract.Withdraw(); + + this.mTransactionExecutor.Verify(s => s.Transfer(this.mContractState.Object, this.tokenHolder, 100), Times.Once); + Assert.Equal(0ul, contract.GetDividends()); + Assert.Equal(100ul, contract.GetTotalDividends()); + } + } +} diff --git a/Testnet/DividendToken/DividendToken.Tests/InMemoryState.cs b/Testnet/DividendToken/DividendToken.Tests/InMemoryState.cs new file mode 100644 index 00000000..cc1cbffa --- /dev/null +++ b/Testnet/DividendToken/DividendToken.Tests/InMemoryState.cs @@ -0,0 +1,82 @@ +using NBitcoin; +using Stratis.SmartContracts; +using System; +using System.Collections.Generic; + +namespace DividendTokenContract.Tests +{ + public class InMemoryState : IPersistentState + { + private readonly Dictionary storage = new Dictionary(); + + public void Clear(string key) => this.storage.Remove(key); + + public T GetValue(string key) => (T)this.storage.GetValueOrDefault(key, default(T)); + + public Address GetAddress(string key) => this.GetValue
(key); + + public T[] GetArray(string key) => this.GetValue(key); + + public bool GetBool(string key) => this.GetValue(key); + + public byte[] GetBytes(byte[] key) => throw new NotImplementedException(); + + public byte[] GetBytes(string key) => this.GetValue(key); + + public char GetChar(string key) => this.GetValue(key); + + public int GetInt32(string key) => this.GetValue(key); + + public long GetInt64(string key) => this.GetValue(key); + + public string GetString(string key) => this.GetValue(key); + + public T GetStruct(string key) + where T : struct => this.GetValue(key); + + public uint GetUInt32(string key) => this.GetValue(key); + + public ulong GetUInt64(string key) => this.GetValue(key); + + public UInt128 GetUInt128(string key) => this.GetValue(key); + + public UInt256 GetUInt256(string key) => this.GetValue(key); + + public bool IsContract(Address address) + { + throw new NotImplementedException(); + } + + public void SetAddress(string key, Address value) => this.storage.AddOrReplace(key, value); + + public void SetArray(string key, Array a) => this.storage.AddOrReplace(key, a); + + public void SetBool(string key, bool value) => this.storage.AddOrReplace(key, value); + + public void SetBytes(byte[] key, byte[] value) + { + throw new NotImplementedException(); + } + + public void SetBytes(string key, byte[] value) => this.storage.AddOrReplace(key, value); + + public void SetChar(string key, char value) => this.storage.AddOrReplace(key, value); + + public void SetInt32(string key, int value) => this.storage.AddOrReplace(key, value); + + public void SetInt64(string key, long value) => this.storage.AddOrReplace(key, value); + + public void SetString(string key, string value) => this.storage.AddOrReplace(key, value); + + public void SetStruct(string key, T value) + where T : struct => this.storage.AddOrReplace(key, value); + + public void SetUInt32(string key, uint value) => this.storage.AddOrReplace(key, value); + + public void SetUInt64(string key, ulong value) => this.storage.AddOrReplace(key, value); + + public void SetUInt128(string key, UInt128 value) => this.storage.AddOrReplace(key, value); + + public void SetUInt256(string key, UInt256 value) => this.storage.AddOrReplace(key, value); + } +} diff --git a/Testnet/DividendToken/DividendToken.sln b/Testnet/DividendToken/DividendToken.sln new file mode 100644 index 00000000..fff0a409 --- /dev/null +++ b/Testnet/DividendToken/DividendToken.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30204.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DividendToken", "DividendToken\DividendToken.csproj", "{3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DividendToken.Tests", "DividendToken.Tests\DividendToken.Tests.csproj", "{3B5AF108-6986-4E17-9A01-EB297D6F0C15}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AD529F2-7163-4BBE-80BB-F2D7D54B2DFA}.Release|Any CPU.Build.0 = Release|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B5AF108-6986-4E17-9A01-EB297D6F0C15}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA15305F-3348-4660-BF5B-3D0CD51F0FFD} + EndGlobalSection +EndGlobal diff --git a/Testnet/DividendToken/DividendToken/DividendToken.cs b/Testnet/DividendToken/DividendToken/DividendToken.cs new file mode 100644 index 00000000..2592979d --- /dev/null +++ b/Testnet/DividendToken/DividendToken/DividendToken.cs @@ -0,0 +1,304 @@ +using Stratis.SmartContracts; +using Stratis.SmartContracts.Standards; + +[Deploy] +public class DividendToken : SmartContract, IStandardToken +{ + + public ulong Dividends + { + get => PersistentState.GetUInt64(nameof(this.Dividends)); + private set => PersistentState.SetUInt64(nameof(this.Dividends), value); + } + + private Account GetAccount(Address address) => PersistentState.GetStruct($"Account:{address}"); + + private void SetAccount(Address address, Account account) => PersistentState.SetStruct($"Account:{address}", account); + + + public DividendToken(ISmartContractState state, UInt256 totalSupply, string name, string symbol,uint decimals) + : base(state) + { + this.TotalSupply = totalSupply; + this.Name = name; + this.Symbol = symbol; + this.Decimals = decimals; + this.SetBalance(Message.Sender, totalSupply); + } + + + public override void Receive() + { + DistributeDividends(); + } + + /// + /// It is advised that deposit amount should to be evenly divided by total supply, + /// otherwise small amount of satoshi may lost(burn) + /// + public void DistributeDividends() + { + Dividends += Message.Value; + } + + public bool TransferTo(Address to, UInt256 amount) + { + UpdateAccount(Message.Sender); + UpdateAccount(to); + + return TransferTokensTo(to, amount); + } + + public bool TransferFrom(Address from, Address to, UInt256 amount) + { + UpdateAccount(from); + UpdateAccount(to); + + return TransferTokensFrom(from, to, amount); + } + + private Account UpdateAccount(Address address) + { + var account = GetAccount(address); + var newDividends = GetNewDividends(address, account); + + if (newDividends > 0) + { + account.DividendBalance += newDividends; + account.CreditedDividends = Dividends; + SetAccount(address, account); + } + + return account; + } + + private UInt256 GetWithdrawableDividends(Address address, Account account) + { + return account.DividendBalance + GetNewDividends(address, account); //Delay divide by TotalSupply to final stage for avoid decimal value loss. + } + + private UInt256 GetNewDividends(Address address, Account account) + { + var notCreditedDividends = checked(Dividends - account.CreditedDividends); + return GetBalance(address) * notCreditedDividends; + } + + /// + /// Get Withdrawable dividends + /// + /// + public ulong GetDividends() => GetDividends(Message.Sender); + + /// + /// Get Withdrawable dividends + /// + /// + /// + public ulong GetDividends(Address address) + { + var account = GetAccount(address); + + return GetWithdrawableDividends(address, account) / TotalSupply; + } + + /// + /// Get the all divididends since beginning (Withdrawable Dividends + Withdrawn Dividends) + /// + /// + public ulong GetTotalDividends() => GetTotalDividends(Message.Sender); + + /// + /// Get the all divididends since beginning (Withdrawable Dividends + Withdrawn Dividends) + /// + /// + /// + public ulong GetTotalDividends(Address address) + { + var account = GetAccount(address); + var withdrawable = GetWithdrawableDividends(address, account) / TotalSupply; + return withdrawable + account.WithdrawnDividends; + } + + /// + /// Withdraws all dividends + /// + public void Withdraw() + { + var account = UpdateAccount(Message.Sender); + var balance = account.DividendBalance / TotalSupply; + + Assert(balance > 0, "The account has no dividends."); + + account.WithdrawnDividends += balance; + + account.DividendBalance %= TotalSupply; + + SetAccount(Message.Sender, account); + + var transfer = Transfer(Message.Sender, balance); + + Assert(transfer.Success, "Transfer failed."); + } + + public struct Account + { + /// + /// Withdrawable Dividend Balance. Exact value should to divided by + /// + public UInt256 DividendBalance; + + public ulong WithdrawnDividends; + + /// + /// Dividends computed and added to + /// + + public ulong CreditedDividends; + } + + #region StandardToken code is inlined + + + public string Symbol + { + get => PersistentState.GetString(nameof(this.Symbol)); + private set => PersistentState.SetString(nameof(this.Symbol), value); + } + + public string Name + { + get => PersistentState.GetString(nameof(this.Name)); + private set => PersistentState.SetString(nameof(this.Name), value); + } + + /// + public UInt256 TotalSupply + { + get => PersistentState.GetUInt256(nameof(this.TotalSupply)); + private set => PersistentState.SetUInt256(nameof(this.TotalSupply), value); + } + + public uint Decimals + { + get => PersistentState.GetUInt32(nameof(Decimals)); + private set => PersistentState.SetUInt32(nameof(Decimals), value); + } + + + /// + public UInt256 GetBalance(Address address) + { + return PersistentState.GetUInt256($"Balance:{address}"); + } + + private void SetBalance(Address address, UInt256 value) + { + PersistentState.SetUInt256($"Balance:{address}", value); + } + + /// + private bool TransferTokensTo(Address to, UInt256 amount) + { + if (amount == 0) + { + Log(new TransferLog { From = Message.Sender, To = to, Amount = 0 }); + + return true; + } + + UInt256 senderBalance = GetBalance(Message.Sender); + + if (senderBalance < amount) + { + return false; + } + + SetBalance(Message.Sender, senderBalance - amount); + + SetBalance(to, checked(GetBalance(to) + amount)); + + Log(new TransferLog { From = Message.Sender, To = to, Amount = amount }); + + return true; + } + + /// + private bool TransferTokensFrom(Address from, Address to, UInt256 amount) + { + if (amount == 0) + { + Log(new TransferLog { From = from, To = to, Amount = 0 }); + + return true; + } + + UInt256 senderAllowance = Allowance(from, Message.Sender); + UInt256 fromBalance = GetBalance(from); + + if (senderAllowance < amount || fromBalance < amount) + { + return false; + } + + SetApproval(from, Message.Sender, senderAllowance - amount); + + SetBalance(from, fromBalance - amount); + + SetBalance(to, checked(GetBalance(to) + amount)); + + Log(new TransferLog { From = from, To = to, Amount = amount }); + + return true; + } + + /// + public bool Approve(Address spender, UInt256 currentAmount, UInt256 amount) + { + if (Allowance(Message.Sender, spender) != currentAmount) + { + return false; + } + + SetApproval(Message.Sender, spender, amount); + + Log(new ApprovalLog { Owner = Message.Sender, Spender = spender, Amount = amount, OldAmount = currentAmount }); + + return true; + } + + private void SetApproval(Address owner, Address spender, UInt256 value) + { + PersistentState.SetUInt256($"Allowance:{owner}:{spender}", value); + } + + /// + public UInt256 Allowance(Address owner, Address spender) + { + return PersistentState.GetUInt256($"Allowance:{owner}:{spender}"); + } + + public struct TransferLog + { + [Index] + public Address From; + + [Index] + public Address To; + + public UInt256 Amount; + } + + public struct ApprovalLog + { + [Index] + public Address Owner; + + [Index] + public Address Spender; + + public UInt256 OldAmount; + + public UInt256 Amount; + } + #endregion +} \ No newline at end of file diff --git a/Testnet/DividendToken/DividendToken/DividendToken.csproj b/Testnet/DividendToken/DividendToken/DividendToken.csproj new file mode 100644 index 00000000..227885dc --- /dev/null +++ b/Testnet/DividendToken/DividendToken/DividendToken.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp2.1 + + 8.0 + + + + + + diff --git a/Testnet/DividendToken/README.MD b/Testnet/DividendToken/README.MD new file mode 100644 index 00000000..db698d8e --- /dev/null +++ b/Testnet/DividendToken/README.MD @@ -0,0 +1,11 @@ +# Dividend Token Contract + +**Contract Hash** +``` +41a37d258e324a6c51ec2099a1a20546c696fecf2a649bd6e6cd8add835d46ce +``` + +**Contract Byte Code** +``` +4D5A90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000800000000E1FBA0E00B409CD21B8014CCD21546869732070726F6772616D2063616E6E6F742062652072756E20696E20444F53206D6F64652E0D0D0A2400000000000000504500004C0102001B3479CF0000000000000000E00022200B013000001400000002000000000000B2320000002000000040000000000010002000000002000004000000000000000400000000000000006000000002000000000000030040850000100000100000000010000010000000000000100000000000000000000000603200004F000000000000000000000000000000000000000000000000000000004000000C000000443200001C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000082000004800000000000000000000002E74657874000000B8120000002000000014000000020000000000000000000000000000200000602E72656C6F6300000C00000000400000000200000016000000000000000000000000000040000042000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000094320000000000004800000002000500AC250000980C000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004602280600000A72010000706F0700000A2A4A02280600000A7201000070036F0800000A2A7202280600000A7215000070038C08000001280900000A6F0100002B2A7602280600000A7215000070038C08000001280900000A046F0200002B2AC20203280C00000A0204281700000602052815000006020E0428130000060202280D00000A6F0E00000A0428190000062A1E0228070000062A660202280100000602280D00000A6F0F00000AD728020000062A8E0202280D00000A6F0E00000A280A000006260203280A00000626020304281A0000062A6A0203280A000006260204280A0000062602030405281B0000062A0000133003003C00000001000011020328030000060A020306280C0000060B07166A36241200067B0100000407D77D0100000412000228010000067D030000040203062804000006062A42047B01000004020304280C000006D72A000000133002001800000002000011022801000006047B03000004DB0A0203281800000606D92A4A0202280D00000A6F0E00000A280E0000062A00133003001800000003000011020328030000060A020306280B0000060228160000065C2A4A0202280D00000A6F0E00000A28100000062A00133003001F00000003000011020328030000060A020306280B000006067B02000004D70228160000065C2A001330030093000000040000110202280D00000A6F0E00000A280A0000060A067B010000040228160000065C0B067B010000040228160000065E0C0207166AFE03722D000070281000000A1200067B02000004067B01000004D708DB7D020000041200087D010000040202280D00000A6F0E00000A0628040000060202280D00000A6F0E00000A07281100000A0D02096F1200000A7269000070281000000A2A4602280600000A728B0000706F1300000A2A4A02280600000A728B000070036F1400000A2A4602280600000A72990000706F1300000A2A4A02280600000A7299000070036F1400000A2A4602280600000A72A30000706F0700000A2A4A02280600000A72A3000070036F0800000A2A7202280600000A72BB000070038C08000001280900000A6F0700000A2A7602280600000A72BB000070038C08000001280900000A046F0800000A2A00000013300400A600000005000011042D34021201FE1504000002120102280D00000A6F0E00000A7D040000041201037D050000041201166A7D0600000407280300002B172A0202280D00000A6F0E00000A28180000060A06043402162A0202280D00000A6F0E00000A0604DB281900000602030203281800000604D72819000006021201FE1504000002120102280D00000A6F0E00000A7D040000041201037D050000041201047D0600000407280300002B172A000013300500AA00000006000011052D2A021202FE15040000021202037D040000041202047D050000041202166A7D0600000408280300002B172A020302280D00000A6F0E00000A281E0000060A020328180000060B0605370407053402162A020302280D00000A6F0E00000A0605DB281D00000602030705DB281900000602040204281800000605D72819000006021202FE15040000021202037D040000041202047D050000041202057D0600000408280300002B172A00001330040065000000070000110202280D00000A6F0E00000A03281E000006042E02162A0202280D00000A6F0E00000A0305281D000006021200FE1505000002120002280D00000A6F0E00000A7D070000041200037D080000041200057D0A0000041200047D0900000406280400002B172A8E02280600000A72D3000070038C08000001048C08000001281600000A056F0800000A2A8A02280600000A72D3000070038C08000001048C08000001281600000A6F0700000A2A42534A4201000100000000000C00000076342E302E33303331390000000005006C00000058050000237E0000C40500006C04000023537472696E677300000000300A0000F800000023555300280B0000100000002347554944000000380B00006001000023426C6F6200000000000000020000015717A201090A000000FA0133001600000100000010000000050000000A0000001E0000002700000001000000160000000800000007000000010000000400000008000000010000000300000003000000040000000000A3010100000000000600EE00410306002D0141030600DA0070020F00610300000A001D018C030A00AA038C030E000C0220030A007C038C030A009B008C030A00D3038C0306009100D2010A000E018C030A00AF008C0306007801D2010600B803D2010A0065008C03000000001500000000000100010001001000FE0100001900010001000A011000FF0300002D0001001F000A0110008B0100002D0004001F000A0110007F0100002D0007001F0006003500B6000600F002B60006009F02B6000600F401B90006003402B90006002004B60006005E02B90006004502B90006000F04B60006002004B600502000000000860883024A00010062200000000081089102BD0001007520000000008100F103C20002009220000000008100FC03C9000300B0200000000086186A02D1000500E12000000000C6005B0106000900E920000000008600CA0206000900032100000000E6011B02DA000900272100000000E601D901E1000B004421000000008100E303C2000E008C21000000008100B102EA000F00A0210000000081001003EA001100C42100000000860003034A001300D8210000000086000303F2001300FC21000000008600DE024A0014001022000000008600DE02F20014003C22000000008600350406001500DB22000000008608B501F8001500ED22000000008108C001FC00150000230000000086087A00F800160012230000000081088300FC001600252300000000E6093E044A00170037230000000081084E04BD0017004A2300000000E6014500F2001800672300000000810050000101190088230000000081002602DA001B003C24000000008100E601E1001D00F42400000000E6016301080120006525000000008100970110012300892500000000E6015B001901260000000100550100000100840300000100840300000200070400000100D400000002005E04000003008C0000000400CB0100000100370200000200270400000100F90100000200370200000300270400000100840300000100840300000200070400000100840300000200070400000100840300000100840300000100550100000100550100000100550100000100840300000100840300000200550100000100370200000200270400000100F901000002003702000003002704000001004D02000002001904000003002704000001006402000002004D02000003005501000001006402000002004D0202001D0009006A02010011006A02060019006A020A0029006A02060061006A0206003100C000100069000100150069000B001A007100A30320006900BF0326006900C903320031006A023A0031006E00400081003A02450081004B014A0031002E046600310055026C0051007003740069006B017800690075017D003100930189007100A303A6002E000B0029012E00130032012E001B005101430023005A0181002B005A01A1002B005A01E1002B005A0101012B005A014E00540058005D00830095009C00020001000000160321010000C4012501000087002501000052042101020001000300010002000300020012000500010013000500020014000700010015000700020016000900010017000900048000000000000000000000000000000000AA030000040000000000000000000000AD001E000000000001000200010000000000000000008C0300000000010000000000000000000000000020030000000003000200040002000500020015002D0017002D002B0090002B00A10000000047657455496E7436340053657455496E743634003C4D6F64756C653E0053797374656D2E507269766174652E436F72654C6962004469766964656E6442616C616E63650047657442616C616E63650053657442616C616E636500416C6C6F77616E636500494D657373616765006765745F4D657373616765006765745F4E616D65007365745F4E616D65006E616D650056616C7565547970650049536D617274436F6E74726163745374617465004950657273697374656E745374617465006765745F50657273697374656E7453746174650073746174650044656275676761626C6541747472696275746500436F6D70696C6174696F6E52656C61786174696F6E7341747472696275746500496E646578417474726962757465004465706C6F794174747269627574650052756E74696D65436F6D7061746962696C697479417474726962757465006765745F56616C75650076616C7565005265636569766500417070726F766500476574537472696E6700536574537472696E6700417070726F76616C4C6F67005472616E736665724C6F6700536574417070726F76616C00536D617274436F6E74726163742E646C6C006765745F53796D626F6C007365745F53796D626F6C0073796D626F6C0053797374656D005472616E7366657246726F6D005472616E73666572546F6B656E7346726F6D0066726F6D004469766964656E64546F6B656E00495374616E64617264546F6B656E005472616E73666572546F005472616E73666572546F6B656E73546F00746F006765745F53656E646572005370656E646572007370656E646572005472616E73666572004F776E6572006F776E6572002E63746F720053797374656D2E446961676E6F7374696373006765745F4469766964656E6473007365745F4469766964656E64730043726564697465644469766964656E647300476574576974686472617761626C654469766964656E647300446973747269627574654469766964656E647300476574546F74616C4469766964656E64730057697468647261776E4469766964656E6473004765744469766964656E6473004765744E65774469766964656E647300537472617469732E536D617274436F6E7472616374732E5374616E64617264730053797374656D2E52756E74696D652E436F6D70696C6572536572766963657300446562756767696E674D6F646573006765745F537563636573730041646472657373006164647265737300537472617469732E536D617274436F6E74726163747300466F726D617400536D617274436F6E7472616374004F626A656374004765745374727563740053657453747275637400495472616E73666572526573756C74005570646174654163636F756E74004765744163636F756E74005365744163636F756E74006163636F756E74004F6C64416D6F756E740063757272656E74416D6F756E7400616D6F756E7400417373657274005769746864726177006765745F546F74616C537570706C79007365745F546F74616C537570706C7900746F74616C537570706C7900000000134400690076006900640065006E006400730000174100630063006F0075006E0074003A007B0030007D00003B54006800650020006100630063006F0075006E007400200068006100730020006E006F0020006400690076006900640065006E00640073002E0000215400720061006E00730066006500720020006600610069006C00650064002E00000D530079006D0062006F006C0000094E0061006D006500001754006F00740061006C0053007500700070006C0079000017420061006C0061006E00630065003A007B0030007D00002341006C006C006F00770061006E00630065003A007B0030007D003A007B0031007D00000043222F817C95CB4498A926937E6400A20004200101080320000105200101111104200012350420010B0E052002010E0B0500020E0E1C063001011E000E040A01110C07300102010E1E00052001011225042000124104200011210320000B050702110C0B0307010B040701110C080704110C0B0B122905200201020E072002122911210B032000020420010E0E052002010E0E0507020B111006300101011E00040A0111100607030B0B11100407011114040A0111140600030E0E1C1C087CEC85D7BEA7798E02060B03061121042001010B062001110C1121072002011121110C0820040112250B0E0E0620020211210B08200302112111210B0720020B1121110C0520010B11210320000E042001010E0620020111210B0720030211210B0B08200301112111210B0720020B112111210328000B0328000E0801000800000000001E01000100540216577261704E6F6E457863657074696F6E5468726F77730108010002000000000004010000000000000000000000000000000010000000000000000000000000000000883200000000000000000000A232000000200000000000000000000000000000000000000000000094320000000000000000000000005F436F72446C6C4D61696E006D73636F7265652E646C6C0000000000FF250020001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000C000000B43200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +```