
Arithmétique sur Solana : bonnes pratiques pour créer des applications financières
Sommaire
- Utiliser des entiers et des unités mineures
- Pourcentages
- Intérêts composés
- Dépassements de capacité par valeur supérieure et inférieure
- Qu’est-ce qu’un dépassement de capacité par valeur supérieure ?
- Qu’est-ce qu’un dépassement de capacité par valeur inférieure ?
- Multiplier avant de diviser
- Exemple de marché prédictif
- Appliquer des règles d’arrondi cohérentes
- Particularités de Rust concernant les arrondis
- Arrondi au plus proche ou à l’entier inférieur
- Fonctions arithmétiques saturantes
- Calculer les intérêts sans nombres à virgule flottante
- Conclusion
- Ressources supplémentaires
Merci à 0xIchigo et Lostin pour leur relecture et leur contribution à cet article.
Solana est un environnement concurrentiel : que vous travailliez sur un nouveau protocole de prêt, un agrégateur, un marché prédictif, la tokenisation de RWA ou tout autre projet, il peut être tentant de vous précipiter sur le réseau principal.
Cependant, il est essentiel de garder à l’esprit que les applications blockchain sont, parce qu’elles gèrent de la valeur, intrinsèquement des applications financières.
Si vous n’avez encore jamais créé d’applications financières, que ce soit sur une blockchain ou dans la finance traditionnelle, vous devez connaître l’importance des mathématiques financières.
Il existe de nombreuses ressources de qualité sur les sujets de programmation propres à Solana : vérifier quels comptes doivent signer chaque instruction, quels programmes détiennent les comptes utilisés, éviter les attaques par réouverture de compte, etc. Les contraintes de compte d’Anchor facilitent la mise en œuvre d’un grand nombre de ces vérifications, tandis que Rust propose certains réglages par défaut judicieux, comme la détection des dépassements de capacité par valeur supérieure ou inférieure en mode Debug.
Mais une programmation financière sûre ne se limite pas aux sujets propres à Solana. Une seule erreur dans l’arithmétique des tokens peut provoquer des fuites, une inflation involontaire et le mécontentement des utilisateurs. Sur Solana, où les volumes de transactions sont plus élevés que sur les autres blockchains, les failles peuvent être exploitées encore plus rapidement.
De nombreuses techniques de programmation financière issues de la finance traditionnelle n’ont rien à voir avec la blockchain. Elles ne reçoivent donc pas l’attention qu’elles méritent, mais restent indispensables pour protéger les tokens de vos utilisateurs.
Dans cet article, nous aborderons spécifiquement les sujets suivants :
- L’utilisation d’entiers et d’unités mineures
- La prévention des pertes de précision — multiplier avant de diviser
- L’application de règles d’arrondi cohérentes
- Le calcul des intérêts sans nombres à virgule flottante
Utiliser des entiers et des unités mineures
Le manque de précision est une vulnérabilité courante dans les contrats intelligents. Lorsque les opérations mathématiques manquent de précision, elles peuvent introduire des vulnérabilités. Nous devons utiliser des entiers et des unités mineures pour effectuer des opérations mathématiques sûres dans les applications Solana.
Prenons un exemple simple.
Dans la finance traditionnelle, toutes les opérations que vous avez effectuées « en USD » ont en réalité été réalisées en cents. Si vous utilisiez des GBP, elles étaient réalisées en pence.
De même, vos transactions en SOL doivent être traitées en lamports, et vos transactions en USDC en millionièmes d’USDC.
Les cents, les pence et les lamports sont tous des types d’unités mineures (également appelées unités de base). La raison pour laquelle toutes les opérations doivent être effectuées dans ces unités est simple : les ordinateurs ne savent pas gérer précisément les nombres à virgule flottante.
Voici un en binaire :
| Trente-deux | Seize | Huit | Quatre | Deux | Un |
| 0 | 0 | 0 | 0 | 0 | 1 |
Voici neuf en binaire :
| Trente-deux | Seize | Huit | Quatre | Deux | Un |
| 0 | 0 | 1 | 0 | 0 | 1 |
Mais comment représenter, par exemple, 0,3 USDC ?
La bonne réponse est : c’est impossible :
let answer = 0.1 + 0.2;
msg!("0.1 + 0.2 = {}", answer);Cela donne le résultat suivant :
Journal du programme : "0.1 + 0.2 = 0.30000000000000004"
Considérez plutôt les dollars, les GBP, les SOL, les USDC et toutes les autres « devises » comme des quantités entières de leur unité mineure.
Par exemple, pour additionner 0,1 et 0,2 en utilisant des USDC :
let answer_ints: u128 = 100000 + 200000;
msg!("100000 + 200000 = {}", answer_ints);Cela donne le résultat suivant :
Journal du programme : "100000 + 200000 = 300000"
L’utilisation d’unités mineures évite toute perte.
Les développeurs doivent utiliser le nombre de décimales du token, tout en gardant à l’esprit que tous les tokens n’ont pas le même nombre de décimales.
Utiliser des entiers pour les montants de tokens peut sembler évident, mais vous devez également les utiliser partout, et pas uniquement pour les montants de tokens.
Pourcentages
Les pourcentages doivent être exprimés sous forme de nombres entiers. La plupart des gens utilisent des points de base (parfois appelés « bips »), représentés par bps.
Par exemple, 4,74 % correspond à 474 bps.
Intérêts composés
Les intérêts composés ne doivent pas être calculés avec e (le nombre d’Euler), qui représente la limite des intérêts composés lorsque la fréquence de capitalisation tend vers l’infini.
Si e permet une « capitalisation continue », soit essentiellement une courbe lisse pour les intérêts composés, e est lui-même représenté par un nombre à virgule flottante dans Rust.
En utilisant e, vous obtiendrez à la fois des erreurs d’arrondi et un résultat différent de celui produit par d’autres mécanismes.
Nous présenterons ces deux problèmes plus loin dans cet article.
Dépassements de capacité par valeur supérieure et inférieure
Rust stocke les entiers dans des variables de taille fixe. Cela signifie que, selon que la variable est signée ou non signée, elle ne peut occuper qu’un espace limité en mémoire.
Par exemple, le type u8 peut contenir n’importe quelle valeur comprise entre 0 et 255. Cependant, si nous stockons une valeur située en dehors de cette plage, nous provoquons un dépassement de capacité par valeur supérieure ou inférieure.
Qu’est-ce qu’un dépassement de capacité par valeur supérieure ?
Un dépassement de capacité par valeur supérieure se produit lorsque la valeur dépasse la capacité maximale que le type de variable peut stocker. Elle revient alors à la valeur minimale.
Par exemple, si nous tentions de stocker 256 dans un u8, la valeur reviendrait à 0. 257 deviendrait 1, 258 deviendrait 2, 511 deviendrait 255 et 512 reviendrait à 0.
Qu’est-ce qu’un dépassement de capacité par valeur inférieure ?
Un dépassement de capacité par valeur inférieure se produit lorsque la valeur descend sous la valeur minimale possible et revient à la valeur maximale. Pour un u8, -1 deviendrait 255, -2 deviendrait 254, et ainsi de suite. En bref, ce dépassement ressemble à un dépassement par valeur supérieure, mais son comportement se produit dans la direction opposée.
Rust comporte plusieurs vérifications qui provoquent une panique du programme lors de l’exécution en cas de dépassement de capacité par valeur supérieure ou inférieure, mais elles ne sont pas incluses lorsque vous compilez en mode release. Or la chaîne d’outils intégrée à l’environnement de développement de Solana compile par défaut les programmes Solana en mode release.
Pour en savoir plus sur les dépassements de capacité par valeur supérieure et inférieure, ainsi que sur les moyens de les éviter, consultez notre guide de sécurité des programmes Solana.
Multiplier avant de diviser
Il semble souvent naturel de diviser avant de multiplier. Prenons l’exemple de la création d’un marché prédictif. Lorsqu’un utilisateur parie sur un résultat qui s’avère gagnant, nous devons calculer son paiement. Sur les marchés prédictifs, les gagnants sont rémunérés en fonction de leur part des paris placés sur le résultat gagnant. Mentalement, cela correspond très simplement à :
cagnotte des gains ÷ total des paris sur le résultat gagnant × montant du pari
Cela semble très naturel.
Nous commençons par diviser la cagnotte des gains en petites parts représentant chacune une « part » des gains, puis nous calculons le nombre de parts à attribuer à chaque personne.
Mais si nous commençons par multiplier, nous obtenons un résultat intermédiaire plus élevé. Cela permet de réduire l’impact des erreurs d’arrondi lors de la division suivante.
cagnotte des gains × montant du pari ÷ total des paris sur le résultat gagnant
Exemple de marché prédictif
Voici une courte démonstration. Si ce calcul était effectué par des personnes plutôt que par des ordinateurs, la bonne réponse serait 10,5.
Essayons de commencer par la division :
let answer: u128 = 7 / 2 * 3;
msg!("7 / 2 * 3 = {}", answer);Cela donne le résultat suivant :
Journal du programme : "7 / 2 * 3 = 9"
En commençant par la division, nous perdons 1,5 unité mineure.
Que se passe-t-il si nous commençons par la multiplication ?
let answer: u128 = 7 * 3 / 2;
msg!("7 * 3 / 2 = {}", answer);Cela donne le résultat suivant :
Journal du programme : "7 * 3 / 2 = 10"
En commençant par la multiplication, la perte due à l’arrondi n’est que de 0,5 unité mineure.
Ce principe peut être généralisé : « commencez par toute opération qui augmente l’ordre de grandeur ».
Par exemple, si vous calculez une expression plus complexe qui utilise des puissances ou des racines, comme un calcul de risque, commencez par les puissances et terminez par les racines. En arithmétique à virgule fixe, effectuer la division en premier peut entraîner une perte de précision si le quotient est arrondi à l’entier inférieur avant d’être multiplié.
Appliquer des règles d’arrondi cohérentes
Des règles d’arrondi incohérentes, qui créent de petits écarts au niveau d’un seul token, peuvent donner l’impression que des tokens disparaissent ou apparaissent de nulle part. À terme, cela peut épuiser la liquidité d’un programme ou provoquer une inflation involontaire des tokens.
Comme l’a souligné Will Thieme d’Orca lors de sa présentation à Breakpoint sur les pièges courants des programmes Solana, gagner un token ne semble pas représenter grand-chose.
Cependant, Solana prend en charge de véritables transactions, au sens de la finance traditionnelle, comportant plusieurs instructions et des frais de transaction faibles. Il est donc possible de remplir une transaction de nombreuses instructions individuelles qui exploitent des erreurs de décalage d’une unité, puis de l’exécuter à très faible coût.
Les erreurs d’arrondi sont inévitables, mais des règles d’arrondi cohérentes permettent de les maîtriser. Décidez dès le départ si vous arrondirez à l’entier supérieur ou inférieur et comment vous traiterez les valeurs à mi-chemin, comme 0,5. L’arrondi à l’entier supérieur est plus courant et imposé par certaines normes financières. Le plus important est d’appliquer ces règles de manière cohérente dans l’ensemble de votre code.
Particularités de Rust concernant les arrondis
Les développeurs doivent connaître plusieurs fonctions propres à Rust susceptibles de poser problème, car les opérations d’arrondi constituent une source courante de perte de précision. Le choix de la méthode d’arrondi peut avoir un impact important sur l’exactitude et le comportement de votre programme.
Arrondi au plus proche ou à l’entier inférieur
Par exemple, la fonction try_round_u64() arrondit au nombre entier le plus proche. Si nous voulions créer un programme qui convertit une garantie en liquidité, un arrondi à l’entier supérieur pourrait entraîner la création de plus de tokens de liquidité que ne le justifie la garantie fournie. Nous devons plutôt utiliser la fonction try_floor_u64() pour arrondir au nombre entier inférieur le plus proche.
Fonctions arithmétiques saturantes
De plus, les développeurs utilisent souvent des fonctions arithmétiques saturating_*, comme saturating_add, pour plafonner les valeurs à leurs limites maximales et minimales respectives afin d’éviter les dépassements de capacité par valeur supérieure et inférieure. Toutefois, ces fonctions peuvent entraîner de subtiles pertes de précision.
Par exemple, si votre fonction multiplie le montant d’une transaction par un multiplicateur de récompense et que le produit dépasse la valeur maximale autorisée par le type de variable, la récompense accordée à votre utilisateur sera insuffisante.
Il est essentiel de garder cela à l’esprit, en particulier parce que votre programme doit utiliser l’arithmétique à virgule fixe.
Calculer les intérêts sans nombres à virgule flottante
Une technique courante pour calculer les intérêts composés consiste à utiliser le nombre d’Euler e, mais e est un nombre à virgule flottante ! Évitez d’utiliser de tels nombres pour calculer les intérêts. Utilisez plutôt l’arithmétique à virgule fixe.
La bibliothèque spl-math de Solana fournit PreciseNumber qui, comme vous pouvez l’imaginer, fonctionne dans l’environnement Rust plus limité de Solana et peut représenter de minuscules fractions décimales, jusqu’à 12 décimales, tout en conservant une précision exacte.
use spl_math::precise_number::PreciseNumber;
fn calculate_compound_interest(
principal: u128,
rate_basis_points: u128,
time: u128,
compounds_per_year: u128,
) -> u128 {
// Formula: result = principal * (1 + rate/compounds_per_year)^(compounds_per_year * time)
// Where rate_basis_points is expressed in basis points (500 for 5%)
// Convert principal to PreciseNumber
let principal = PreciseNumber::new(principal).unwrap();
// Convert basis points to decimal percentage (divide by 10000)
let rate = PreciseNumber::new(rate_basis_points)
.unwrap()
.checked_div(&PreciseNumber::new(10_000).unwrap())
.unwrap();
// Calculate rate/compounds_per_year
let rate_per_period = rate
.checked_div(&PreciseNumber::new(compounds_per_year).unwrap())
.unwrap();
// Calculate 'base', which is 1 + rate/compounds_per_year
let one = PreciseNumber::new(1).unwrap();
let base = rate_per_period.checked_add(&one).unwrap();
// Calculate 'total_periods', which is compounds_per_year * time
let total_periods = compounds_per_year.checked_mul(time).unwrap();
// Calculate compound_factor, which is (1 + rate/compounds_per_year)^(compounds_per_year * time)
let compound_factor = base.checked_pow(total_periods).unwrap();
// Calculate result = principal * compound_factor
principal
.checked_mul(&compound_factor)
.unwrap()
.to_imprecise()
.unwrap()
}
Vous pouvez observer les différences en exécutant le code :
- Journal du programme : "Utilisation de spl-math
PreciseNumber" - Journal du programme : "Investissement de 1 000 $ à 5 % pendant 5 ans, capitalisé 1 fois par an :"
- Journal du programme : "Montant final : 1 276 $"
À comparer avec l’utilisation de e :
- Journal du programme : " Utilisation de e (à ne pas faire)"
- Journal du programme : "Investissement de 1 000 $ à 5 % pendant 5 ans, capitalisé 1 fois par an :"
- Journal du programme : "Montant final : 1 284 $"
Conclusion
L’arithmétique des tokens est un sujet rébarbatif, mais la négliger peut provoquer le genre de rebondissements dont vous vous passeriez volontiers. Vos applications on-chain doivent gérer les tokens avec la précision et la sécurité exigées par les utilisateurs.
Dans cet article, nous avons abordé l’utilisation des entiers et des unités mineures, la multiplication avant la division, la prévention des pertes de précision, l’application de règles d’arrondi cohérentes et le calcul des intérêts sans nombres à virgule flottante.
Après avoir assimilé ces principes fondamentaux d’arithmétique, faites examiner votre code par un tiers, plus précisément par une société d’audit spécialisée dans Solana, avant de le lancer sur le réseau principal.
Ressources supplémentaires
Articles associés
Abonnez-vous à Helius
Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication


