Aktualisiert am 14. Juni 2024: Ein Community-Mitglied hat diesen Blog sorgfältig geprüft und Informationen zu dem ADU-Vorfall bereitgestellt, der eine neue Form darstellt, die in unserer vorherigen Kategorisierung nicht abgedeckt war. Vielen Dank, und jedes aufschlussreiche Feedback ist willkommen!
Zur Verbesserung der Marktstabilität sind Reflexionstoken (auch Belohnungstoken genannt) darauf ausgelegt, Investoren einen zusätzlichen Weg zur Einkommensgenerierung zu bieten. Dies ermutigt Investoren, ihre Token zu halten, anstatt sie zu handeln. Während der berüchtigten Meme-Coin-Saison 2021 wurden Reflexionstoken zu einem unverzichtbaren Mechanismus, der nach der Einführung auf Plattformen wie DxSale (z. B. SafeMoon V1) schnell die Aufmerksamkeit des Marktes auf sich zog.
Obwohl die Aufregung nachließ und sich der Markt 2023 abkühlte, erkannte unser System dutzende von Tausenden von Hacking-Vorfällen, die solche Token-Mechanismen in freier Wildbahn ausnutzten. Diese Reaper-ähnlichen Angriffe führten zwar im Vergleich zu anderen Arten von DeFi-Angriffen zu relativ geringen Verlusten, resultierten jedoch in nicht unerheblichen Verlusten für Nutzervermögen.
In diesem Blog konzentrieren wir uns hauptsächlich auf die Weitergabe von sicherheitsbezogenen Erkenntnissen aus unserer Forschung. Insbesondere geben wir zunächst eine kurze Einführung in den Mechanismus des Reflexionstokens. Danach werden wir Sicherheitsvorfälle im Zusammenhang mit Reflexionstoken überprüfen, wobei der Schwerpunkt auf denen liegt, die den Reflexionstoken-Mechanismus ausnutzen. Anschließend werden wir ein potenzielles Sicherheitsproblem theoretisch erörtern. Abschließend werden wir einige Gedanken zur Abmilderung und zu Lösungen teilen.
0x1 Mechanismus des Reflexionstokens
Unseres Wissens wurde dieser Mechanismus erstmals von Reflect Finance eingeführt, um einen Prozentsatz des Transaktionsbetrags als Gebühren an alle Token-Inhaber auf nicht-transaktionale Weise zu verteilen. Im März 2021 wurde der renommierte SafeMoon V1 auf der BNB-Kette veröffentlicht, was das Reflexionstoken weiter popularisierte.
0x1.1 Grundkonzepte
Bevor wir uns mit den Details befassen, sollten einige grundlegende Konzepte eingeführt werden, um ein besseres Verständnis zu erzielen.
Es gibt zwei Arten von Räumen: r-space und t-space, gelesen als reflektierter Raum und wahrer Raum. Die Kryptowährungen zweier Räume haben Wechselkurse, die auf dem relativen Umlaufvolumen basieren. Außerdem ist die Währung im r-space deflationär, das heißt, bei jeder Transaktion wird ein bestimmter Prozentsatz verbrannt, und infolgedessen wird der verbrannte Betrag vom Umlaufvolumen abgezogen.
Betrachten Sie, dass Alice, Bob und Eve alle Transaktionen in beiden Räumen durchführen können, wie in der folgenden Abbildung gezeigt. Wenn Alice und Bob paarweise Transfers im t-space durchführen, erhält Eve keine Belohnungen. Wenn jedoch alle drei zuerst ihre Token in r-space umwandeln und dann Alice und Bob sich gegenseitig übertragen lassen, erhält Eve schließlich ein passives Einkommen, indem sie ihre Token zurück in t-space umwandelt. Das ist die Grundidee des Reflexionstoken-Mechanismus.
Beachten Sie, dass nicht alle Konten, wie z. B. der Liquiditätsbereitstellungspool des Tokens, in r-space handeln können, d. h. bestimmte Konten müssen von r-space ausgeschlossen werden.
0x1.2 Vertragsebene Erklärung
Nun wollen wir uns den REFLECT-Vertrag von Reflect Finance ansehen, um diesen Mechanismus zu untersuchen.
Dieser Vertrag definiert zunächst mehrere Variablen für die Kontoverwaltung:
mapping (address => uint256) private _rOwned; // reflektierte Token, die vom Benutzer gehalten werden
mapping (address => uint256) private _tOwned; // wahre Token, die vom Benutzer gehalten werden
mapping (address => mapping (address => uint256)) private _allowances;
mapping (address => bool) private _isExcluded; // ob der Benutzer vom r-space ausgeschlossen ist
address[] private _excluded; // Konten, die vom r-space ausgeschlossen sind
Dann definiert er die wesentlichen Konstanten des Vertrags. Es ist ersichtlich, dass _rTotal auf ein bestimmtes Vielfaches von _tTotal (d. h. die totalSupply des Tokens, die als Rückgabewert der totalSupply-Funktion verwendet wird) gesetzt ist:
uint256 private constant MAX = ~uint256(0);
uint256 private constant _tTotal = 10 * 10**6 * 10**9;
uint256 private _rTotal = (MAX - (MAX % _tTotal));
Aus funktionaler Sicht und aus Sicht der Benutzerinteraktion fallen die Funktionen dieses Vertrags in die folgenden drei Kategorien: Guthabenabfrage und Token-Übertragung sowie eine eindeutige Reflexionsfunktion. Die beiden erstgenannten sind mit dem ERC-20-Standard kompatibel, jedoch variiert die interne Logik von anderen ERC-20-Token. Jede dieser Funktionen wird nachfolgend näher erläutert.
0x1.2.1 Funktionen für Guthabenabfrage
Die Guthabenberechnung unterscheidet sich für ausgeschlossene und nicht ausgeschlossene Benutzer:
function balanceOf(address account) public view override returns (uint256) {
if (_isExcluded[account]) return _tOwned[account];
return tokenFromReflection(_rOwned[account]);
}
function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
require(rAmount <= _rTotal, "Amount must be less than total reflections");
uint256 currentRate = _getRate();
return rAmount.div(currentRate);
}
Dies kann durch die folgende Formel ausgedrückt werden:

Der Kurs in der obigen Formel wird durch Aufruf der Funktion _getRate berechnet, die tatsächlich aus dem Rückgabewert der Funktion _getCurrentSupply innerhalb des Vertrags berechnet wird.
function _getRate() private view returns(uint256) {
(uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
return rSupply.div(tSupply);
}
function _getCurrentSupply() private view returns(uint256, uint256) {
uint256 rSupply = _rTotal;
uint256 tSupply = _tTotal;
for (uint256 i = 0; i < _excluded.length; i++) {
if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply) return (_rTotal, _tTotal);
rSupply = rSupply.sub(_rOwned[_excluded[i]]);
tSupply = tSupply.sub(_tOwned[_excluded[i]]);
}
if (rSupply < _rTotal.div(_tTotal)) return (_rTotal, _tTotal);
return (rSupply, tSupply);
}
Es ist nicht schwierig, die entsprechende Formel aus dem obigen Code-Snippet abzuleiten:

0x1.2.2 Funktionen für Token-Übertragung
Im Allgemeinen gibt es vier Szenarien für die Übertragung von Vermögenswerten:
function _transfer(address sender, address recipient, uint256 amount) private {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
require(amount > 0, "Transfer amount must be greater than zero");
if (_isExcluded[sender] && !_isExcluded[recipient]) {
_transferFromExcluded(sender, recipient, amount); // t-space -> r-space
} else if (!_isExcluded[sender] && _isExcluded[recipient]) {
_transferToExcluded(sender, recipient, amount); // r-space -> t-space
} else if (!_isExcluded[sender] && !_isExcluded[recipient]) {
_transferStandard(sender, recipient, amount); // r-space -> r-space
} else if (_isExcluded[sender] && _isExcluded[recipient]) {
_transferBothExcluded(sender, recipient, amount); // t-space -> t-space
} else {
_transferStandard(sender, recipient, amount); // r-space -> r-space
}
}
Für ausgeschlossene Konten müssen sowohl _rOwned als auch _tOwned zum jeweiligen Raum addiert oder davon subtrahiert werden. Für nicht ausgeschlossene Konten muss nur _rOwned berücksichtigt werden. Zum Beispiel zeigt der folgende Code-Schnipsel die Implementierung der Übertragung von Vermögenswerten von r-space zu t-space, wobei sender ein nicht ausgeschlossenes Konto und recipient ein ausgeschlossenes Konto ist.
function _transferToExcluded(address sender, address recipient, uint256 tAmount) private {
(uint256 rAmount, uint256 rTransferAmount, uint256 rFee, uint256 tTransferAmount, uint256 tFee) = _getValues(tAmount);
_rOwned[sender] = _rOwned[sender].sub(rAmount);
_tOwned[recipient] = _tOwned[recipient].add(tTransferAmount);
_rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);
_reflectFee(rFee, tFee);
emit Transfer(sender, recipient, tTransferAmount);
}
-
Die Funktion
_getValuesberechnet den entsprechenden Betrag, den Transferbetrag und die Gebühr (d. h. Betrag = Transferbetrag + Gebühr) für beide Räume.function _getValues(uint256 tAmount) private view returns (uint256, uint256, uint256, uint256, uint256) { (uint256 tTransferAmount, uint256 tFee) = _getTValues(tAmount); uint256 currentRate = _getRate(); (uint256 rAmount, uint256 rTransferAmount, uint256 rFee) = _getRValues(tAmount, tFee, currentRate); return (rAmount, rTransferAmount, rFee, tTransferAmount, tFee); } -
Die Funktion
_reflectFeereflektiert die Gebühr im r-space. Speziell wird_rTotalum die Gebühr im r-space (d. h.rFee) reduziert, was wiederum den Kurs senkt. Gemäß der Berechnungsmethode von balanceOf(user) werden Gebühren auf diese Weise an alle nicht ausgeschlossenen Token-Inhaber reflektiert.function _reflectFee(uint256 rFee, uint256 tFee) private { _rTotal = _rTotal.sub(rFee); _tFeeTotal = _tFeeTotal.add(tFee); }
0x1.2.3 Die Funktion reflect
Zusätzlich zur passiven Auslösung des Reflexionstoken-Mechanismus während des Übertragungsprozesses können Benutzer die Funktion reflect aktiv aufrufen, um diesen Mechanismus zu initiieren. Speziell wenn jemand die von ihm gehaltenen Token verbraucht, um diese Funktion aufzurufen, sinkt der Kurs, da rSupply sinkt, was anderen Token-Inhabern Vorteile bringt. Mit anderen Worten, sich selbst für andere opfern. Dadurch können die Projektmanager Token-Inhaber durch die Nutzung dieser Funktion incentivieren.
function reflect(uint256 tAmount) public {
address sender = _msgSender();
require(!_isExcluded[sender], "Excluded addresses cannot call this function");
(uint256 rAmount,,,,,,) = _getValues(tAmount);
_rOwned[sender] = _rOwned[sender].sub(rAmount);
_rTotal = _rTotal.sub(rAmount);
_tFeeTotal = _tFeeTotal.add(tAmount);
}
Die oben dargestellten Codes bilden den Kern des Mechanismus. Unterschiedliche Token können spezifische zusätzliche Funktionen enthalten, um ihre Implementierung anzupassen. Zum Beispiel können einige Transaktionsgebühren verwenden, um eine "Swap and Liquify"-Funktion zu betreiben und Panik zu verhindern, wenn Wale beschließen, ihre Token zu verkaufen.
0x2 Post-Mortem von Rekt Reflexionstoken
Wie bereits erwähnt, konzentrieren wir uns hauptsächlich auf Angriffe, die den Reflexionstoken-Mechanismus ausnutzen. Daher werden Vorfälle, die nicht mit diesem Mechanismus zusammenhängen, wie der SafeMoon V2-Angriff (ein häufiges ERC20-Problem des öffentlichen Burns) und der jüngste ZongZi-Angriff (bezüglich einer altmodischen Preismanipulation durch Ausnutzung des Spotpreises), nicht behandelt.
Wir haben diese Angriffe eingehend analysiert, um ihre Ursachen zu entmystifizieren. Eine Liste aller dieser Vorfälle finden Sie hier. Wir haben festgestellt, dass die meisten von ihnen normale Sicherheitsvorfälle sind, die entweder durch Code-Schwachstellen oder durch unsachgemäße administrative Operationen verursacht wurden. Einige sind jedoch sehr verdächtig (z. B. das Vorhandensein einer Hintertür), die wir als abnormale Sicherheitsvorfälle bezeichnen. In den folgenden Unterabschnitten werden wir zuerst die normalen Sicherheitsvorfälle vorstellen und dann die abnormalen im Detail behandeln.
0x2.1 Normale Sicherheitsvorfälle
Unsere Untersuchung deutet darauf hin, dass diese Vorfälle aus zwei Arten von Problemen resultieren, d. h. aus Problemen auf Code-Ebene und auf Operationsebene, entweder einzeln oder in Kombination.
-
Probleme auf Code-Ebene. Dies ergibt sich aus der schlampigen Implementierung des Vertrags, wahrscheinlich weil die Entwickler den Mechanismus des Reflexionstokens nicht vollständig verstanden haben, was zu einer Inkonsistenz zwischen dem tatsächlichen Angebot an Token und dem aufgezeichneten Wert von
totalSupplyführt, der zur Manipulation des Kurses verwendet werden kann:-
1.1 Nullkosten-Burn
-
1.2 Zusätzliche Abzüge von
rSupplywährend Token-Übertragungen -
1.3 Verwechslung zwischen r-space und t-space Werten (mit Präzisionsverlust zur Gewinnerzielung)
-
-
Probleme auf Operationsebene. Dies resultiert aus unsachgemäßer Bedienung durch Administratoren. Speziell bezieht sich dies bei diesen Vorfällen auf die unsachgemäße Konfiguration der AMM-Pair-Adressen, die nicht ordnungsgemäß ausgeschlossen sind.
Es ist erwähnenswert, dass ALLE aufgeführten Probleme auf Code-Ebene zu Inkonsistenzen zwischen dem tatsächlichen Angebot und totalSupply führen können, wodurch die Verträge anfällig werden. Dies bedeutet jedoch nicht unbedingt, dass diese Schwachstellen ausnutzbar sind oder, genauer gesagt, profitabel genug, um sie auszunutzen, da es für den Angreifer Kosten geben kann, den Kurs zu manipulieren. Vereinfacht ausgedrückt werden wir im Folgenden den Begriff "ausnutzbar" verwenden, um "profitabel genug, um es auszunutzen" zu bedeuten. Infolgedessen ist ein Problem auf der Operationsebene in einigen Szenarien erforderlich, um diese Schwachstellen ausnutzbar zu machen.
Speziell kann Problem 1.1 direkt ausgenutzt werden, während die Probleme 1.2 und 1.3 eine Kombination mit Problem 2 erfordern, um ausnutzbar zu werden. Daher können die diskutierten Vorfälle basierend auf diesen Beobachtungen in zwei Typen, Typ-I und Typ-II, unterteilt werden. Nachfolgend finden Sie eine Tabelle mit den relevanten Daten:
| Typ | Vorfall(e) | Ursachen | Anzahl (%) |
|---|---|---|---|
| I | CATOSHI | Nur Problem auf Code-Ebene (1.1) | 1 (0.79%) |
| II (a) | BEVO, FETA, ADU | Kombination aus beidem (1.2 & 2) | 3 (2.38%) |
| II (b) | SHEEP und 120+ andere | Kombination aus beidem (1.3 & 2) | 122 (96.83%) |
Die Tabelle zeigt, dass Vorfälle vom Typ II einen erheblichen Anteil ausmachen. Speziell gibt es 2 Variationen innerhalb von Typ II: Typ-II-a (d. h. Problem 1.2 mit Problem 2) und Typ-II-b (d. h. Problem 1.3 mit Problem 2). Darüber hinaus deuten die Exploits für SHEEP-ähnliche Vorfälle der Kategorie Typ-II-b darauf hin, dass Angreifer (z. B. dieser) möglicherweise automatisierte Methoden verwenden, um ähnlich anfällige Verträge zu identifizieren. Spezifische Details werden in den folgenden Unterabschnitten behandelt.
0x2.1.1 Typ-I: Nur Problem auf Code-Ebene (Problem 1.1)
Nur ein Vorfall gehört zu Typ I, d. h. der CATOSHI-Vorfall, ein Nullkosten-Burn, der die Gesamtmenge betrifft.
Werfen wir zunächst einen Blick auf die Funktion burnOf im CATOSHI-Vertrag:
function burnOf(uint256 tAmount) public {
uint256 currentRate = _getRate();
uint256 rAmount = tAmount.mul(currentRate);
_tTotal = _tTotal.sub(tAmount);
_rTotal = _rTotal.sub(rAmount);
emit Transfer(_msgSender(), address(0), tAmount);
}
Offensichtlich wird der von dieser Funktion verbrannte Betrag nicht vom Anrufer (d. h. msg.sender) abgezogen. _rOwned[msg.sender] sollte jedoch um rAmount reduziert werden, und wenn das Konto ausgeschlossen ist, sollte _tOwned[msg.sender] ebenfalls um tAmount reduziert werden.
Aufgrund dieses Versehen können Angreifer zunächst eine große Menge an Token kostenlos verbrennen und dann die reflect-Funktion des Vertrags aufrufen. Da sowohl _tTotal als auch _rTotal proportional stark reduziert wurden:

Der Kurs kann durch Aufrufen der Funktion reflect leicht nach unten manipuliert werden, wodurch der balanceOf(Angreifer) erheblich steigt. Dies ermöglicht es Angreifern, von dem aufgeblähten Guthaben zu profitieren.

Warum? Beachten Sie, dass das neue Guthaben des Angreifers wie folgt berechnet wird:

Das Verhältnis zwischen balanceOf(Angreifer) und balanceOf(Angreifer)' ist:

Da

Daher

Das bedeutet, der Angreifer sammelt mehr Token, die gegen wertvolle Token (in diesem Fall WETH) getauscht werden können, um Gewinne zu erzielen.
0x2.1.2 Typ-II-a: Kombination aus Problem 1.2 und Problem 2
Vorfall vom Typ II-a beinhalten die Kombination zweier Probleme:
- Problem 1.2: Zusätzliche Abzüge von
rSupplywährend Token-Übertragungen. - Problem 2: AMM-Paar ist nicht ausgeschlossen.
Bei Typ II-a gibt es drei Angriffsereignisse, die gemäß den Schwachstellenformen in Problem 1.2 weiter in zwei Unterkategorien unterteilt werden können, wie folgt:
1. Zusätzliche Reflexion in der _reflectFee Funktion
Zwei Vorfälle gehören zu dieser Unterkategorie, d. h. der BEVO-Vorfall und der FETA-Vorfall. Im Folgenden verwenden wir den BEVO-Vertrag zur Veranschaulichung.
Wie in 'Funktionen für Token-Übertragungen' (Abschnitt 0x1.2.2) eingeführt, löst jede Token-Übertragung die Reflexion eines Teils der Transaktionsgebühr aus. In BEVO werden neben der ursprünglichen Gebühr noch zwei weitere Teile berücksichtigt: Burn und Charity.
function _reflectFee(uint256 rFee, uint256 rBurn, uint256 rCharity, uint256 tFee, uint256 tBurn, uint256 tCharity) private {
_rTotal = _rTotal.sub(rFee).sub(rBurn).sub(rCharity); // rChairty wird von _rTotal abgezogen
_tFeeTotal = _tFeeTotal.add(tFee);
_tBurnTotal = _tBurnTotal.add(tBurn);
_tCharityTotal = _tCharityTotal.add(tCharity);
_tTotal = _tTotal.sub(tBurn);
}
Beachten Sie, dass das Spendenkonto ausgeschlossen ist, was bedeutet, dass der an dieses Konto gesendete Betrag verbrannt wird, wie in der Funktion _sendToCharity gezeigt.
function _sendToCharity(uint256 tCharity, address sender) private {
uint256 currentRate = _getRate();
uint256 rCharity = tCharity.mul(currentRate);
address currentCharity = _charity[0];
_rOwned[currentCharity] = _rOwned[currentCharity].add(rCharity);
_tOwned[currentCharity] = _tOwned[currentCharity].add(tCharity); // da das Spendenkonto ausgeschlossen ist, wird der Spendenanteil verbrannt
emit Transfer(sender, currentCharity, tCharity);
}
Aus dem obigen Code-Schnipsel können wir sehen, dass es zwei Stellen gibt, um den Spendenanteil zu reflektieren und zu verbrennen, was dazu führt, dass das tatsächliche Token-Angebot während der Übertragungen inkonsistent mit dem totalSupply wird. Wenn mehr Token übertragen werden, ist der Wert von rSupply aufgrund der zusätzlichen Abnahme geringer als das Angebot an Token im Pool.
Die rein theoretische Beschreibung mag etwas abstrakt sein, also verwenden wir ein Beispiel, um den Prozess zu verdeutlichen. Angenommen, Alice möchte 10 Token an Bob übertragen, und 3 Token werden wie folgt abgezogen: 1 für die Gebühr, 1 für den Burn und 1 für wohltätige Zwecke. Da der Spendenanteil sowohl reflektiert als auch verbrannt wird, beträgt die tatsächliche Aufschlüsselung 2 Token, die reflektiert werden (1 Gebühr + 1 Spende), und 2 Token, die verbrannt werden (1 Burn + 1 Spende). Zusammen mit den verbleibenden 7 Token, die an Bob übertragen werden sollen, sind insgesamt 11 Token an diesem Prozess beteiligt, was fehlerhaft ist.
Aber warum kann diese Inkonsistenz ausgenutzt werden, um Gewinne zu erzielen? Nachfolgend werden wir mit unserem mathematischen Zauberstab die Konsequenzen ableiten.
Angenommen, wir haben zuvor einige Token aus dem Pool (d. h. PancakeSwap-Paar) erworben, bezeichnet als rAmount im r-space und tAmount im t-space. Da der Pool nicht ausgeschlossen wurde, bezeichnen wir _rOwned[pair] als rReserve, wobei der entsprechende Wert im t-space ebenfalls als tReserve bezeichnet wird. Dann gilt:

Aufgrund der zusätzlichen Abzüge ist rSupply nun geringer als das Angebot an Token im Pool:

Erinnern Sie sich an den Abschnitt 'Funktionen für Guthabenabfrage' (0x1.2.1), der aktuelle Kurs kann mit der folgenden Formel berechnet werden:

Zu diesem Zeitpunkt, wenn wir die von uns gehaltenen Token über die Funktion reflect (die in diesem Vertrag in deliver umbenannt wurde) reflektieren, wird der Kurs zu rate':

Da

Dann haben wir

Durch die Kombination der Formeln 1, 3 und 6 können wir die folgende Ungleichung ableiten:

Dies bedeutet, dass die Anzahl der Token, die wir direkt aus dem Pool (über die Funktion skim) entnehmen können, größer ist als das, was wir geliefert haben, was profitabel ist, da die Kosten für den Aufruf der Funktion reflect gedeckt werden können. Danach kann der Angreifer die entnommenen Token gegen wertvolle Token (in diesem Fall WBNB) tauschen, um einen Gewinn zu erzielen.
Beachten Sie, dass der BEVO-Vertrag auch für Problem 1.3 anfällig ist, das im Angriff nicht ausgenutzt wurde.
2. Falsche rTransferAmount-Berechnung in der _getRValues Funktion
Nur ein Vorfall gehört zu dieser Form, d. h. der ADU-Vorfall. Werfen wir zunächst einen Blick auf den folgenden Code-Schnipsel.
function _transferStandard(address sender, address recipient, uint256 tAmount) private {
(uint256 rAmount, uint256 rTransferAmount, uint256 rFee, uint256 tTransferAmount, uint256 tFee, uint256 tTeam) = _getValues(tAmount);
_rOwned[sender] = _rOwned[sender].sub(rAmount);
_rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);
_takeTeam(tTeam);
_reflectFee(rFee, tFee);
emit Transfer(sender, recipient, tTransferAmount);
}
function _getValues(uint256 tAmount) private view returns (uint256, uint256, uint256, uint256, uint256, uint256) {
(uint256 tTransferAmount, uint256 tFee, uint256 tTeam) = _getTValues(tAmount, _taxFee, _teamFee);
uint256 currentRate = _getRate();
(uint256 rAmount, uint256 rTransferAmount, uint256 rFee) = _getRValues(tAmount, tFee, currentRate);
return (rAmount, rTransferAmount, rFee, tTransferAmount, tFee, tTeam);
}
function _getTValues(uint256 tAmount, uint256 taxFee, uint256 teamFee) private pure returns (uint256, uint256, uint256) {
uint256 tFee = tAmount.mul(taxFee).div(100);
uint256 tTeam = tAmount.mul(teamFee).div(100);
uint256 tTransferAmount = tAmount.sub(tFee).sub(tTeam); // tTeam wird von tAmount abgezogen
return (tTransferAmount, tFee, tTeam);
}
function _getRValues(uint256 tAmount, uint256 tFee, uint256 currentRate) private pure returns (uint256, uint256, uint256) {
uint256 rAmount = tAmount.mul(currentRate);
uint256 rFee = tFee.mul(currentRate);
uint256 rTransferAmount = rAmount.sub(rFee); // Allerdings wird kein rTeam von rAmount abgezogen
return (rAmount, rTransferAmount, rFee);
}
Wir sehen, dass sowohl die Steuergebühr als auch die Teamgebühr bei Übertragungen abgezogen werden sollten. In der Funktion _getTvalues wird tTransferAmount jedoch um tFee und tTeam subtrahiert, während in der Funktion _getRValues nur rFee subtrahiert wird. Diese Diskrepanz führt zu dem zuvor erwähnten Inkonsistenzproblem, das sich mit zunehmender Anzahl von Token-Übertragungen verschlimmert.
Da das Paar auch nicht vom Token ausgeschlossen ist, ist dieser Token ausnutzbar. Insbesondere könnte ein Angreifer ähnliche BEVO-Exploits verwenden, um mehr ADU-Token über die skim-Funktion des Paares zu erhalten, nachdem die deliver-Funktion aufgerufen wurde.
Angesichts des damaligen On-Chain-Status war es dem Angreifer jedoch nicht möglich, die entnommenen ADU-Token gegen WBNB zu tauschen, um einen Gewinn zu erzielen (aufgrund der require-Anweisung in der Funktion tokenFromReflection). Daher müsste der Angreifer eine komplexere Ausbeutungsstrategie anwenden, die hier nicht detailliert wird.
function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
require(rAmount <= _rTotal, "Amount must be less than total reflections");
uint256 currentRate = _getRate();
return rAmount.div(currentRate);
}
0x2.1.3 Typ-II-b: Kombination aus Problem 1.3 und Problem 2
Vorfall vom Typ II-b beinhalten die Kombination zweier Probleme:
- Problem 1.3: Verwechslung zwischen r-space und t-space Werten (beachten Sie, dass auch ein Präzisionsverlust vorhanden sein muss, um Gewinne zu erzielen).
- Problem 2: AMM-Paar ist nicht ausgeschlossen.
Problem 1.3 ergibt sich aus der fehlerhaften Handhabung von Werten zwischen r-space und t-space während der Implementierung der internen _burn-Funktion.
function burn(uint256 _value) public {
_burn(msg.sender, _value);
}
function _burn(address _who, uint256 _value) internal {
require(_value <= _rOwned[_who]);
_rOwned[_who] = _rOwned[_who].sub(_value); // _rOwned sollte um einen r-space Wert reduziert werden
_tTotal = _tTotal.sub(_value); // _tTotal sollte um einen t-space Wert reduziert werden
// Für die Semantik der burn-Funktion sollte auch _rTotal von einem r-space Wert abgezogen werden.
emit Transfer(_who, address(0), _value);
}
Unter Berücksichtigung der wesentlichen Konstanten des Vertrags ist der r-space Wert typischerweise ein Vielfaches des t-space Werts. Daher wird durch Aufrufen der burn-Funktion mit einem _value, das in der Größenordnung von tSupply liegt, der Kurs erheblich aufgebläht.
Im Gegensatz zu den Fällen vom Typ I kann der Anrufer jedoch keine Token verbrennen, während sein eigenes Guthaben unverändert bleibt. Mit anderen Worten, es ist schwierig, wenn nicht unmöglich, für den Angreifer, mehr Token zu ernten. Wie könnten also die Fälle vom Typ II-b ausnutzbar sein?
Am Beispiel des SHEEP-Token-Vorfalls. Der Wert der SHEEP-Token, die vom Angreifer gehalten werden, kann bezeichnet werden als:

Wobei der Preis von SHEEP durch den Spotpreis im PancakeSwap-Paar ausgedrückt werden kann, berechnet als:

Dann kann der Wert weiter ausgedrückt werden als:

Der Angreifer führt dann wiederholt die burn-Funktion aus und synchronisiert schließlich das Paar. Da weder der Angreifer noch das Paar ausgeschlossen sind, sinken ihre Guthaben aufgrund der von uns erwähnten Kurs-Inflation. Somit wird das Verhältnis angepasst auf:

Basierend auf den vorherigen Definitionen können wir diese Verhältnisse weiter ausdrücken als:

Wobei X die Summe der verbrannten _value darstellt.
Hier kommt die Magie: Das letztere Verhältnis ist schlichtweg kleiner als das erstere, wenn wir 8 und 9 weiter vereinfachen, was uns verwundert, da der Gewinn in diesem Fall ein negativer Wert wäre:

Tatsächlich nutzte der Angreifer einen Präzisionsverlust-Fehler im Reflexionstoken aus. Für nicht ausgeschlossene Benutzer wird, gemäß der von uns bereitgestellten Formel, die Guthabenberechnung in der tokenFromReflection-Funktion tatsächlich abgerundet. Somit kann der Rückgabewert der balanceOf-Abfrage kleiner sein als sein theoretischer Wert. Das heißt, das Verhältnis' kann größer sein als das Verhältnis, wenn wir dieses Problem berücksichtigen.
function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
require(rAmount <= _rTotal, "Amount must be less than total reflections");
uint256 currentRate = _getRate();
return rAmount.div(currentRate);
}
Durch das Debugging der Angriffstransaktion können wir das theoretische Guthaben des Angreifers und des Paares vor und nach diesen Manipulationen berechnen. Die Ergebnisse unserer Berechnungen sind in der folgenden Tabelle aufgeführt:
Δ in der Tabelle ist ein extrem kleiner Wert, weit weniger als 1.
Durch die Analyse des Trace innerhalb der sync-Funktion des Paares können wir berechnen, dass die theoretischen Guthaben des Angreifers und des Paares tatsächlich 27,523 bzw. 2,972 betragen, was zu einem Verhältnis von 9,26 führt. Aufgrund des Präzisionsverlusts werden die Guthaben jedoch auf 27 bzw. 2 abgerundet, was das Verhältnis auf 13,50 aufbläht. Infolgedessen wird der Gewinn zu einem positiven Wert.
Schließlich kann der Angreifer durch einen umgekehrten Swap Gewinn erzielen.
0x2.2 Anormale Sicherheitsvorfälle
In diesem Unterabschnitt werden wir unsere Erkenntnisse aus der Untersuchung der FDP- und DBALL-Token teilen. Unsere Analyse deutet darauf hin, dass die Manager beider FDP- und DBALL-Token problematische privilegierte Funktionen aufgerufen haben, die effektiv als Hintertüren fungierten und die Projekte gefährdeten und letztendlich zu Angriffen führten. Insbesondere im DBALL-Projekt haben wir eine Reihe verdächtiger Transaktionen des Token-Besitzers identifiziert, die klare Beweise dafür liefern, dass es sich um einen Rug Pull handelt.
Die Ausnutzungen, die auf diese beiden Token abzielten, ähneln stark denen, die im Abschnitt 'Typ-II-a: Kombination aus Problem 1.2 und Problem 2' unter 0x2.1.2 beschrieben wurden. Bei der Analyse der Gründe, warum das tatsächliche Token-Angebot von totalSupply abweichen kann, fallen jedoch einige verdächtige Aktivitäten auf.
0x2.2.1 Der FDP-Vorfall
Die Diskrepanz zwischen dem tatsächlichen Token-Angebot und totalSupply im FDP-Fall beruht auf dem Aufruf der Funktion transferOwnership, einer privilegierten Funktion, die nur vom Vertragseigentümer aufgerufen werden kann. Wie der Name schon sagt, soll diese Funktion das Eigentum am Vertrag ändern. In dem FDP-Vertrag hat diese Funktion jedoch nichts mit der Eigentumsübertragung zu tun. Stattdessen erhöht sie _rOwned[newOwner], ohne totalSupply zu ändern. Dies verstößt eindeutig gegen die Designprinzipien des normalen Token-Minting-Prozesses.
function transferOwnership(address newOwner) public virtual onlyOwner {
(, uint256 rTransferAmount,, uint256 tTransferAmount,,) = _getValues(_tTotal);
_rOwned[newOwner] = _rOwned[newOwner].add(rTransferAmount);
emit Transfer(address(0), newOwner, tTransferAmount);
}
Transaktionen, die diese Funktion aufgerufen haben, sind in der folgenden Tabelle zusammengefasst:
| Zeitstempel | Transaktions-Hash | Anrufer | Der newOwner |
|---|---|---|---|
| 2021-06-05 17:23:57 | 0x46fa1f97...4606d9bc | 0xef309c...262586 | 0x9e96af...24481a |
| 2021-06-05 17:24:06 | 0x686e0d82...d6ebfb62 | 0xef309c...262586 | 0xb0c426...a72063 |
| 2021-06-05 17:26:12 | 0x44285339...7a526320 | 0xef309c...262586 | 0xef309c...262586 |
| 2021-06-05 19:39:00 | 0xaff7a688...dfe3f344 | 0xef309c...262586 | 0x9e96af...24481a |
| 2022-06-05 18:08:10 | 0x2c413604...f7718f25 | 0xef309c...262586 | 0xef309c...262586 |
| 2022-06-05 18:49:16 | 0x8f4309ca...97d4bcec | 0xef309c...262586 | 0xef309c...262586 |
| 2022-06-05 23:33:44 | 0xaa029544...3ff7b629 | 0xef309c...262586 | 0xef309c...262586 |
0x2.2.2 Der DBALL-Vorfall
Die Dinge werden für den DBALL-Fall knifflig. Unter Verwendung von MetaSleuth zur Analyse des Geldflusses des DBALL-Besitzers beobachteten wir ein Ungleichgewicht bei Zu- und Abflüssen von DBALL-Token zu dieser Adresse, wobei die Mittelherkunft für diese Transaktion nicht aufgezeichnet wurde.
Durch Abfragen der historischen Zustände auf der Kette identifizierten wir schließlich, dass sich das DBALL-Guthaben des Besitzers vor und nach dieser Transaktion änderte. Wir können beobachten, dass der Besitzer die privilegierte manualDevBurn-Funktion aufgerufen hat, um 1 Token im t-space zu verbrennen. Die Implementierung dieser Funktion ist wie folgt:
function manualDevBurn (uint256 tAmount) public onlyOwner() {
uint256 rAmount = tAmount.mul(_getRate());
if (_isExcluded[_msgSender()]) {
_tOwned[_msgSender()] = _tOwned[_msgSender()] - (tAmount);
}
_rOwned[_msgSender()] = _rOwned[_msgSender()] - (rAmount);
_tOwned[address(0)] = _tOwned[address(0)] + (tAmount);
_rOwned[address(0)] = _rOwned[address(0)] + (rAmount);
emit Transfer(_msgSender(), address(0), tAmount);
}
Auf den ersten Blick scheint alles in Ordnung zu sein. Aufgrund der Angabe, dass der Vertrag eine Compilerversion unter 0.8 verwendet, tritt jedoch ein arithmetischer Unterlauf während der Subtraktion von _rOwned[_msgSender()] auf, der von 0 auf fast type(uint256).max wechselt. Diese subtile Manipulation ermöglicht es dem Eigentümer, sein Guthaben zu ändern, führt aber auch zu einer Inkonsistenz im Token-Angebot.
Ist das nur ein versehentlicher Fehler? Unsere Untersuchung deutet darauf hin, dass es sich eher um einen absichtlichen Rug Pull handelt. Die Gründe sind wie folgt zusammengefasst:
-
Der Besitzer hat nur 1 Token in die Funktion
manualDevBurnübergeben, doch innerhalb einer halben Stunde wurde über diese Transaktion eine Menge DBALL, die der Gesamtmenge entspricht, an eine zugehörige Adresse übertragen. -
Diese zugehörige Adresse tauschte sofort im PancakeSwap-Paar und erhielt ungefähr 56 WBNB.
- Die Analyse der Geldflüsse dieser beiden Adressen zeigt, dass beide schließlich BNB über Tornado.Cash transferierten.
0x3 Ein potenzielles Problem bei der Kursberechnung
Abgesehen von den bereits diskutierten Vorfällen haben wir auch festgestellt, dass theoretisch ein potenzielles Problem bei der Guthabenberechnung für nicht ausgeschlossene Benutzer besteht, das eine weitere Diskussion verdient. Dieses Problem kann bei der Berechnung des Kurses auftreten.
Werfen wir einen Blick auf die Funktion _getCurrentSupply. In dieser Funktion bestimmt die if-Anweisung am Ende, ob rSupply geringer ist als der Anfangskurs (d. h. _rTotal / _tTotal).
function _getRate() private view returns(uint256) {
(uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
return rSupply.div(tSupply);
}
function _getCurrentSupply() private view returns(uint256, uint256) {
uint256 rSupply = _rTotal;
uint256 tSupply = _tTotal;
for (uint256 i = 0; i < _excluded.length; i++) {
if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply) return (_rTotal, _tTotal);
rSupply = rSupply.sub(_rOwned[_excluded[i]]);
tSupply = tSupply.sub(_tOwned[_excluded[i]]);
}
if (rSupply < _rTotal.div(_tTotal)) return (_rTotal, _tTotal);
return (rSupply, tSupply);
}
Während der Lebensdauer von der Vertragsbereitstellung bis zur Projektveröffentlichung würden bei Erfüllung dieser Bedingung die Guthaben aller nicht ausgeschlossenen Benutzer Null sein. Sobald das Projekt jedoch gestartet und Transaktionen begonnen haben, ging die ursprüngliche Absicht der if-Anweisung verloren.
Da rSupply aufgrund des Reflexionstoken-Mechanismus sinkt, sinkt auch der Kurs entsprechend. Wenn nach einer bestimmten Transaktion rSupply unter den anfänglichen Kurs fällt, springt der aktuelle Kurs an, was zu Guthabenverlusten für alle nicht ausgeschlossenen Benutzer führt. Darüber hinaus ist es theoretisch möglich, dass

Dies führt dazu, dass der Kurs aufgrund von Präzisionsverlust auf Null gesetzt wird, was möglicherweise eine Division-durch-Null-Panik auslöst.
0x4 Abmilderung und Lösungen
Der Reflexionstoken-Mechanismus bietet eine Möglichkeit, die Marktstabilität zu verbessern, indem er Investoren dazu anregt, ihre Token zu halten, anstatt sie zu handeln, um zusätzliche Belohnungen zu erhalten. Er führt jedoch auch neue Sicherheitsherausforderungen und potenzielle Risiken ein, wie z. B. Verwechslungen zwischen r-space und t-space Werten. Daher ist es für Blockchain-Entwickler und Investoren entscheidend, ein besseres Verständnis des Mechanismus und seiner potenziellen Risiken zu erlangen und nach Lösungen zu suchen.
BlockSec bietet Sicherheitsdienste und Produkte für die Phasen vor und nach dem Start. Unsere Sicherheitsaudit-Dienste führen gründliche Überprüfungen durch, um die Codesicherheit und Transparenz zu gewährleisten. Unser Phalcon-Produkt bietet kontinuierliche Sicherheitsüberwachung und Angriffserkennungsfunktionen, die es Betreibern und Investoren ermöglichen, Projekte zu überwachen und automatische Maßnahmen zu ergreifen, wenn Sicherheitsrisiken erkannt werden.
Verwandte Lektüre
- Wie L2-Blockchains besser tun können, um ihre Benutzer zu schützen
- Top 10 "Awesome" Sicherheitsvorfälle im Jahr 2023
- Konzeptionelle vollständige Analyse: Der Aufstieg von Bitcoin mit Inschriften
Über BlockSec
BlockSec ist ein Full-Stack-Web3-Sicherheitsdienstleister. Das Unternehmen hat sich der Verbesserung von Sicherheit und Benutzerfreundlichkeit für die aufkommende Web3-Welt verschrieben, um deren Massenadoption zu erleichtern. Zu diesem Zweck bietet BlockSec Sicherheitsaudit-Dienste für Smart Contracts und EVM-Ketten, die Phalcon-Plattform zur Sicherheitsentwicklung und zur proaktiven Abwehr von Bedrohungen, die MetaSleuth-Plattform zur Nachverfolgung und Untersuchung von Geldern und die MetaSuites-Erweiterung für Web3-Entwickler, um effizient im Krypto-Bereich zu agieren.
Bisher hat das Unternehmen über 300 Kunden wie die Uniswap Foundation, Compound, Forta und PancakeSwap bedient und in zwei Finanzierungsrunden Millionen von US-Dollar von namhaften Investoren wie Matrix Partners, Vitalbridge Capital und Fenbushi Capital erhalten.
-
Website: https://blocksec.com/
-
E-Mail: [email protected]
-
Twitter: https://twitter.com/BlockSecTeam
-
MetaSleuth: https://metasleuth.io/
-
MetaSuites: https://blocksec.com/metasuites



