Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Solnet.Programs/Solnet.Programs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@
<Folder Include="Models\TokenSwap\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Runtime.Extensions" Version="4.3.1" />
</ItemGroup>

<Import Project="..\..\SharedBuildProperties.props" />
</Project>
2 changes: 1 addition & 1 deletion src/Solnet.Programs/Stake/StakeProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static class StakeProgram
/// <summary>
/// Stake Config ID
/// </summary>
public static readonly PublicKey ConfigKey = new("StakeConfig11111111111111111111111111111111");
public static readonly PublicKey ConfigKey = new("StakeConfig11111111111111111111111111111111");
/// <summary>
/// The program's name.
/// </summary>
Expand Down
29 changes: 29 additions & 0 deletions src/Solnet.Programs/StakePool/Models/AccountType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Solnet.Programs.StakePool.Models
{
/// <summary>
/// Enum representing the account type managed by the program.
/// </summary>
public enum AccountType : byte
{
/// <summary>
/// If the account has not been initialized, the enum will be 0.
/// </summary>
Uninitialized = 0,

/// <summary>
/// Stake pool.
/// </summary>
StakePool = 1,

/// <summary>
/// Validator stake list.
/// </summary>
ValidatorList = 2
}
}
133 changes: 133 additions & 0 deletions src/Solnet.Programs/StakePool/Models/Fee.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Numerics;

namespace Solnet.Programs.StakePool.Models
{
/// <summary>
/// Fee rate as a ratio, minted on UpdateStakePoolBalance as a proportion of the rewards.
/// If either the numerator or the denominator is 0, the fee is considered to be 0.
/// </summary>
public class Fee
{
/// <summary>
/// Denominator of the fee ratio.
/// </summary>
public ulong Denominator { get; set; }

/// <summary>
/// Numerator of the fee ratio.
/// </summary>
public ulong Numerator { get; set; }

/// <summary>
/// Returns true if the fee is considered zero (either numerator or denominator is zero).
/// </summary>
public bool IsZero => Denominator == 0 || Numerator == 0;

/// <summary>
/// Initializes a new instance of the <see cref="Fee"/> class.
/// </summary>
public Fee() { }

/// <summary>
/// Initializes a new instance of the <see cref="Fee"/> class with the specified numerator and denominator.
/// </summary>
/// <param name="numerator"></param>
/// <param name="denominator"></param>
public Fee(ulong numerator, ulong denominator)
{
Numerator = numerator;
Denominator = denominator;
}

/// <summary>
/// Applies the fee's rates to a given amount, returning the amount to be subtracted as fees.
/// Returns 0 if denominator is 0 or amount is 0, or null if overflow occurs.
/// </summary>
public ulong? Apply(ulong amount)
{
if (Denominator == 0 || amount == 0)
return 0;

try
{
// Use BigInteger to avoid overflow
BigInteger amt = new BigInteger(amount);
BigInteger numerator = new BigInteger(Numerator);
BigInteger denominator = new BigInteger(Denominator);

BigInteger feeNumerator = amt * numerator;
// Ceiling division: (feeNumerator + denominator - 1) / denominator
BigInteger result = (feeNumerator + denominator - 1) / denominator;

if (result < 0 || result > ulong.MaxValue)
return null;

return (ulong)result;
}
catch
{
return null;
}
}

/// <summary>
/// Checks withdrawal fee restrictions, throws StakePoolFeeException if not met.
/// </summary>
public void CheckWithdrawal(Fee oldWithdrawalFee)
{
// Constants as per SPL Stake Pool program
var WITHDRAWAL_BASELINE_FEE = new Fee(1, 1000); // 0.1%
var MAX_WITHDRAWAL_FEE_INCREASE = new Fee(3, 2); // 1.5x

ulong oldNum, oldDenom;
if (oldWithdrawalFee.Denominator == 0 || oldWithdrawalFee.Numerator == 0)
{
oldNum = WITHDRAWAL_BASELINE_FEE.Numerator;
oldDenom = WITHDRAWAL_BASELINE_FEE.Denominator;
}
else
{
oldNum = oldWithdrawalFee.Numerator;
oldDenom = oldWithdrawalFee.Denominator;
}

// Check that new_fee / old_fee <= MAX_WITHDRAWAL_FEE_INCREASE
try
{
BigInteger left = (BigInteger)oldNum * Denominator * MAX_WITHDRAWAL_FEE_INCREASE.Numerator;
BigInteger right = (BigInteger)Numerator * oldDenom * MAX_WITHDRAWAL_FEE_INCREASE.Denominator;

if (left < right)
{
throw new StakePoolFeeException("Fee increase exceeds maximum allowed.");
}
}
catch
{
throw new StakePoolFeeException("Calculation failure in withdrawal fee check.");
}
}

/// <summary>
/// Returns a string representation of the fee.
/// </summary>
public override string ToString()
{
if (Numerator > 0 && Denominator > 0)
return $"{Numerator}/{Denominator}";
return "none";
}
}

/// <summary>
/// Exception for stake pool fee errors.
/// </summary>
public class StakePoolFeeException : System.Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="StakePoolFeeException"/> class with a specified error message.
/// </summary>
/// <param name="message"></param>
public StakePoolFeeException(string message) : base(message) { }
}
}
166 changes: 166 additions & 0 deletions src/Solnet.Programs/StakePool/Models/FeeType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System;

namespace Solnet.Programs.StakePool.Models
{
/// <summary>
/// The type of fees that can be set on the stake pool.
/// </summary>
public abstract class FeeType
{
/// <summary>
/// Represents a referral fee type with a specified percentage.
/// </summary>
/// <remarks>This class is used to define a referral fee as a percentage value. The percentage is
/// immutable and must be specified at the time of instantiation.</remarks>
public class SolReferral : FeeType
{
/// <summary>
/// Gets the percentage value represented as a byte.
/// </summary>
public byte Percentage { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SolReferral"/> class with the specified referral
/// percentage.
/// </summary>
/// <param name="percentage">The referral percentage to be applied. Must be a value between 0 and 100, inclusive.</param>
public SolReferral(byte percentage) => Percentage = percentage;
}

/// <summary>
/// Represents a referral fee type with a specified percentage for staking rewards.
/// </summary>
/// <remarks>This class is used to define a referral fee as a percentage of staking rewards. The
/// percentage value is immutable and must be specified at the time of instantiation.</remarks>
public class StakeReferral : FeeType
{
/// <summary>
/// Gets the percentage value represented as a byte.
/// </summary>
public byte Percentage { get; }
/// <summary>
/// Represents a referral with a specified stake percentage.
/// </summary>
/// <remarks>The <paramref name="percentage"/> parameter defines the proportion of the
/// stake allocated to the referral.</remarks>
/// <param name="percentage">The percentage of the stake associated with the referral. Must be a value between 0 and 100.</param>
public StakeReferral(byte percentage) => Percentage = percentage;
}

/// <summary>
/// Represents an epoch in the fee structure, associated with a specific fee.
/// </summary>
/// <remarks>This class is used to define a specific epoch and its corresponding fee. It inherits
/// from the <see cref="FeeType"/> base class.</remarks>
public class Epoch : FeeType
{
/// <summary>
/// Gets the fee associated with the transaction.
/// </summary>
public Fee Fee { get; }
/// <summary>
/// Represents a specific epoch with an associated fee.
/// </summary>
/// <param name="fee">The fee associated with the epoch. Cannot be null.</param>
public Epoch(Fee fee) => Fee = fee;
}

/// <summary>
/// Represents a withdrawal of staked funds, including an associated fee.
/// </summary>
/// <remarks>This class encapsulates the details of a stake withdrawal operation, including the
/// fee applied to the withdrawal. It inherits from <see cref="FeeType"/>.</remarks>
public class StakeWithdrawal : FeeType
{
/// <summary>
/// Gets the fee associated with the transaction.
/// </summary>
public Fee Fee { get; }
/// <summary>
/// Initializes a new instance of the <see cref="StakeWithdrawal"/> class with the specified fee.
/// </summary>
/// <param name="fee">The fee associated with the stake withdrawal. This value cannot be null.</param>
public StakeWithdrawal(Fee fee) => Fee = fee;
}

/// <summary>
/// Represents a deposit fee type specific to Solana transactions.
/// </summary>
/// <remarks>This class encapsulates the fee information for a Solana deposit transaction. It
/// inherits from the <see cref="FeeType"/> base class.</remarks>
public class SolDeposit : FeeType
{
/// <summary>
/// Gets the fee associated with the transaction.
/// </summary>
public Fee Fee { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SolDeposit"/> class with the specified fee.
/// </summary>
/// <param name="fee">The fee associated with the deposit. This value cannot be null.</param>
public SolDeposit(Fee fee) => Fee = fee;
}

/// <summary>
/// Represents a deposit required for staking, associated with a specific fee.
/// </summary>
/// <remarks>This class encapsulates the concept of a staking deposit, which includes a fee that
/// must be paid. It inherits from <see cref="FeeType"/>, providing additional context for fee-related
/// operations.</remarks>
public class StakeDeposit : FeeType
{
/// <summary>
/// Gets the fee associated with the transaction.
/// </summary>
public Fee Fee { get; }
/// <summary>
/// Initializes a new instance of the <see cref="StakeDeposit"/> class with the specified fee.
/// </summary>
/// <param name="fee">The fee associated with the stake deposit. Cannot be null.</param>
public StakeDeposit(Fee fee) => Fee = fee;
}

/// <summary>
/// Represents a withdrawal operation for Solana (SOL) that includes an associated fee.
/// </summary>
/// <remarks>This class encapsulates the details of a Solana withdrawal, including the fee
/// required for the transaction.</remarks>
public class SolWithdrawal : FeeType
{
/// <summary>
/// Gets the fee associated with the transaction.
/// </summary>
public Fee Fee { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SolWithdrawal"/> class with the specified fee.
/// </summary>
/// <param name="fee">The fee associated with the withdrawal. This value cannot be null.</param>
public SolWithdrawal(Fee fee) => Fee = fee;
}

/// <summary>
/// Checks if the provided fee is too high.
/// </summary>
public bool IsTooHigh()
{
return this switch
{
SolReferral s => s.Percentage > 100,
StakeReferral s => s.Percentage > 100,
Epoch e => e.Fee.Numerator > e.Fee.Denominator,
StakeWithdrawal s => s.Fee.Numerator > s.Fee.Denominator,
SolWithdrawal s => s.Fee.Numerator > s.Fee.Denominator,
SolDeposit s => s.Fee.Numerator > s.Fee.Denominator,
StakeDeposit s => s.Fee.Numerator > s.Fee.Denominator,
_ => false
};
}

/// <summary>
/// Returns true if the contained fee can only be updated earliest on the next epoch.
/// </summary>
public bool CanOnlyChangeNextEpoch()
{
return this is StakeWithdrawal or SolWithdrawal or Epoch;
}
}
}
29 changes: 29 additions & 0 deletions src/Solnet.Programs/StakePool/Models/FundingType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Solnet.Programs.StakePool.Models
{
/// <summary>
/// Represents the type of funding operation in the stake pool.
/// </summary>
public enum FundingType
{
/// <summary>
/// A deposit of a stake account.
/// </summary>
StakeDeposit = 0,

/// <summary>
/// A deposit of SOL tokens.
/// </summary>
SolDeposit = 1,

/// <summary>
/// A withdrawal of SOL tokens.
/// </summary>
SolWithdraw = 2,
}
}
Loading
Loading