Bericht – Manifest
| Element | Beschreibung |
|---|---|
| Kunde | Radiant Capital |
| Ziel | Radiant V2 |
Versionshistorie
| Version | Datum | Beschreibung |
|---|---|---|
| 1.0 | 15. März 2023 | Erste Version |
| 2.0 | 21. März 2023 | Zweite Version |
1. Einleitung
1.1 Über Sicherheitstests
Wir wurden von Radiant Capital beauftragt, Sicherheitstests (als Red Team) für die Smart Contracts von Radiant V2 durchzuführen, um potenzielle Risiken zu identifizieren. Als verantwortungsbewusstes Team nimmt Radiant Capital die Sicherheit ernst. Daher entschied sich das Team, zusätzliche Anstrengungen in die Absicherung dieser Smart Contracts zu investieren, obwohl diese bereits von mehreren Sicherheitsunternehmen geprüft wurden ^1.
Beachten Sie, dass sich Sicherheitstests in Zielen und Anforderungen von Sicherheitsaudits unterscheiden. Insbesondere zielen Sicherheitstests darauf ab, zusätzliche/ungewöhnliche Schwachstellen durch Nachahmung von Angreifern zur Umgehung des Programms/Protokolls zu entdecken, während Sicherheitsaudits eine relativ umfassende Sicherheitsprüfung durch Auflistung möglicher Angriffsflächen zum Ziel haben. Daher können Sicherheitstests aufgrund der begrenzten Zeit und Ressourcen nicht alle komplizierten Logikfehler abdecken, die bei einem Sicherheitsaudit identifiziert werden könnten.
1.2 Über Zielverträge
| Information | Beschreibung |
|---|---|
| Typ | Smart Contract |
| Sprache | Solidity |
| Ansatz | Statische Analyse, dynamische Analyse, semi-automatische und manuelle Verifizierung |
Das Ziel-Repository ist Radiant_v2.1.1. Die Commit-SHA-Werte während der Sicherheitstests sind unten aufgeführt. Unser Bericht ist für die ursprüngliche Version (d. h. Version 1) sowie für neue Codes (in den folgenden Versionen) zur Behebung von Problemen im Bericht verantwortlich.

Beachten Sie, dass dieser Bericht nur Smart Contracts im Ordner radiant_v2.1.1/contracts dieses Repositorys abdeckt, darunter:
- bounties
- deployments
- flashloan
- leverage
- lock
- oracles
- staking
- zap
- eligibility
- misc
- oft
- protocol
- stargate
Nach dem Update in Version 8 umfassen die in diesen Sicherheitstests abgedeckten Dateien:
- lending/AaveOracle.sol
- lending/AaveProtocolDataProvider.sol
- lending/ATokensAndRatesHelper.sol
- lending/StableAndVariableTokensHelper.sol
- lending/UiPoolDataProviderV2V3.sol
- lending/UiPoolDataProvider.sol
- lending/WETHGateway.sol
- lending/WalletBalanceProvider.sol
- lending/configuration
- lending/flashloan
- lending/lendingpool
- lending/tokenization
- radiant/accessories
- radiant/eligibility
- radiant/oracles
- radiant/staking
- radiant/token
- radiant/zap
1.3 Sicherheitsmodell
Zur Risikobewertung folgen wir den Standards oder Vorschlägen, die sowohl in der Industrie als auch in der akademischen Welt weit verbreitet sind, einschließlich der OWASP Risk Rating Methodology ^2 und Common Weakness Enumeration ^3. Die Gesamtschwere des Risikos wird durch Wahrscheinlichkeit und Auswirkung bestimmt. Insbesondere wird die Wahrscheinlichkeit verwendet, um abzuschätzen, wie wahrscheinlich eine bestimmte Schwachstelle von einem Angreifer entdeckt und ausgenutzt werden kann, während die Auswirkung verwendet wird, um die Folgen eines erfolgreichen Angriffs zu messen.
In diesem Bericht werden sowohl Wahrscheinlichkeit als auch Auswirkung in zwei Ratings kategorisiert, d. h. hoch und niedrig bzw. deren Kombinationen, wie in Tabelle 1.1 gezeigt.

Entsprechend werden die in diesem Bericht gemessenen Schweregrade in drei Kategorien klassifiziert: Hoch, Mittel, Niedrig. Der Vollständigkeit halber wird Unbestimmt auch verwendet, um Umstände abzudecken, bei denen das Risiko nicht gut bestimmt werden kann.
Darüber hinaus fällt der Status eines entdeckten Elements in eine der folgenden vier Kategorien:
-
Unbestimmt Noch keine Antwort.
-
Bestätigt Das Element wurde vom Kunden erhalten, aber noch nicht bestätigt.
-
Bestätigt Das Element wurde vom Kunden anerkannt, aber noch nicht behoben.
-
Behoben Das Element wurde vom Kunden bestätigt und behoben.
2. Automatisierte Sicherheitstests
2.1 Automatisierte statische Sicherheitstests
Wir verwenden unser internes statisches Analysewerkzeug basierend auf Slither, um auf das Vorhandensein von Schwachstellen zu prüfen. Nach manueller Überprüfung der Ergebnisse wurden keine Probleme festgestellt. Detaillierte Testergebnisse finden Sie in Tabelle 4.1 im Anhang.
2.2 Automatisierte dynamische Sicherheitstests
Wir nutzen Fuzzing-Techniken, um die Robustheit, Zuverlässigkeit und Präzision der Zielverträge zu testen. Insbesondere wird der anfängliche Seed im Fuzzing-Prozess basierend auf der Funktionssemantik und den Vertragstest-Skripten bestimmt. Um die On-Chain-Umgebung zu simulieren, pflegen wir auch eine Reihe von Adressen, die mit dem Vertrag LendingPool und MultiFeeDistribution interagiert haben.
Unser Fuzzer berücksichtigt auch die Funktionssemantik bei der
Transaktionssequenzgenerierung. Zum Beispiel sind die Funktionen
stake im Vertrag MultiFeeDistribution und deposit im Vertrag
LendingPool wahrscheinlich zuerst in der Sequenz aufzurufen. Die
Mutation der Funktionsparameter und -sequenz wird durch die
Codeabdeckung des Vertrags gesteuert. Wenn ein bestimmter Parameter oder
eine Sequenz eine höhere Codeabdeckung erreicht, hat sie eine höhere
Priorität, in der nächsten Fuzzing-Runde mutiert zu werden. Um einige
durch magische Zahlen eingeschränkte Pfade zu untersuchen, sammeln wir
die aus dem Speicher gelesenen Werte (d. h. die SLOAD-Anweisung) zur Laufzeit und
verwenden sie, um Funktionsparameter während des Mutationsprozesses zu
generieren.
Insgesamt generieren wir 100.000 Testfälle und nutzen 31 Orakel, die zur Erkennung von Fehlern verwendet werden. Jeder Testfall enthält 30 Transaktionen mit spezifizierten Reihenfolgen. Schließlich haben wir ein kritisches Problem entdeckt (d. h. Abschnitt 3.2.6), das auch bei unserem manuellen Sicherheitstestprozess entdeckt wurde. Detaillierte Testergebnisse finden Sie in den Tabellen 4.2, 4.3 und 4.4 im Anhang.
3. Manuelle Sicherheitstests
Wir wenden manuelle Anstrengungen an, um das Gesamtkonzept und die Interaktionen zwischen verschiedenen Modulen zu verstehen und führen dann Sicherheitstests basierend auf unserem Know-how über mögliche Angriffsflächen durch, die aus unserer früheren Forschung und Erfahrung abgeleitet wurden.
Insgesamt finden wir siebzehn potenzielle Probleme. Darüber hinaus haben wir drei Empfehlungen und eine Anmerkung wie folgt:
-
Hohes Risiko: 2
-
Mittleres Risiko: 8
-
Niedriges Risiko: 7
-
Empfehlungen: 3
-
Anmerkungen: 1
| ID | Schweregrad | Beschreibung | Kategorie | Status |
|---|---|---|---|---|
| 1 | Mittel | Kein reserviertes Interface zum Zurücksetzen von Funktionszeigern | Software-Sicherheit | Behoben |
| 2 | Mittel | Unsachgemäße Berechnung des Orakels | DeFi-Sicherheit | Behoben |
| 3 | Hoch | Potenzielles Abfließen von Geldern über BaseBounty | DeFi-Sicherheit | Behoben |
| 4 | Niedrig | Mögliche ungültige Emissionspläne | DeFi-Sicherheit | Behoben |
| 5 | Niedrig | Überspringbare Emissionspläne | DeFi-Sicherheit | Bestätigt |
| 6 | Mittel | Veränderbarer Wechselkurs während der Migration | DeFi-Sicherheit | Behoben |
| 7 | Hoch | Unsachgemäße Implementierung von _transfer() (I) | DeFi-Sicherheit | Behoben |
| 8 | Niedrig | Fehlende Prüfung des Zeitraums in UniV2TwapOracle | DeFi-Sicherheit | Behoben |
| 9 | Mittel | Nicht erstattungsfähige Kleinstbeträge | DeFi-Sicherheit | Behoben |
| 10 | Mittel | Unsachgemäße Implementierung von _transfer() (II) | DeFi-Sicherheit | Behoben |
| 11 | Mittel | Manipulierbare Compound-Belohnungen | DeFi-Sicherheit | Behoben |
| 12 | Mittel | Fehlende Zugriffskontrolle in setLeverager() | DeFi-Sicherheit | Behoben |
| 13 | Mittel | Keine Slippage-Prüfung in addLiquidityWETHOnly() | DeFi-Sicherheit | Bestätigt |
| 14 | Niedrig | Fehlende Prüfung des borrowRatio in loopETH() | DeFi-Sicherheit | Behoben |
| 15 | Niedrig | Fehlende Prüfung der Länge zwischen Assets und poolIDs in setPoolIDs() | DeFi-Sicherheit | Behoben |
| 16 | Niedrig | Fehlende Überprüfung des mint-Privilegs bei addBountyContract() | DeFi-Sicherheit | Bestätigt |
| 17 | Niedrig | Minters können nur einmal zugewiesen werden | DeFi-Sicherheit | Bestätigt |
| 18 | - | Gasoptimierung (zapVestingToLp() in Mfd) | Empfehlung | Behoben |
| 19 | - | Nicht-leere Bounty-Reserve in BountyManager | Empfehlung | Behoben |
| 20 | - | Inkonsistente Benennung in requiredUsdValue() | Empfehlung | Bestätigt |
| 21 | - | Veraltete MFDPlus-Anmerkung | Anmerkung | Bestätigt |
Die Details sind in den folgenden Abschnitten aufgeführt.
3.1 Software-Sicherheit
3.1.1 Potenzielles Problem 1: Kein reserviertes Interface zum Zurücksetzen von Funktionszeigern
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | In Version 7 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Drei Funktionen, getLpMfdBounty(), getChefBounty() und
getAutoCompoundBounty(), werden über Funktionszeiger im Vertrag
BountyManager aufgerufen. Gleichzeitig zeigt die Vererbung von
Ownable-Upgradable an, dass dieser Vertrag die Implementierung eines
Proxys sein wird. Dies deutet darauf hin, dass der Implementierungsvertrag
in Zukunft aktualisiert werden kann, was ein Problem im Zusammenhang mit
den Funktionszeigern mit sich bringt.
function initialize(
address _rdnt,
address _weth,
address _lpMfd,
address _mfd,
address _chef,
address _priceProvider,
address _eligibilityDataProvider,
uint256 _hunterShare,
uint256 _baseBountyUsdTarget,
uint256 _maxBaseBounty,
uint256 _bountyBooster
) external initializer {
require(_rdnt != address(0));
require(_weth != address(0));
require(_lpMfd != address(0));
require(_mfd != address(0));
require(_chef != address(0));
require(_priceProvider != address(0));
require(_eligibilityDataProvider != address(0));
require(_hunterShare <= 10000);
require(_baseBountyUsdTarget != 0);
require(_maxBaseBounty != 0);
rdnt = _rdnt;
weth = _weth;
lpMfd = _lpMfd;
mfd = _mfd;
chef = _chef;
priceProvider = _priceProvider;
eligibilityDataProvider = _eligibilityDataProvider;
HUNTER_SHARE = _hunterShare;
baseBountyUsdTarget = _baseBountyUsdTarget;
bountyBooster = _bountyBooster;
maxBaseBounty = _maxBaseBounty;
bounties[1] = getLpMfdBounty;
bounties[2] = getChefBounty;
bounties[3] = getAutoCompoundBounty;
bountyCount = 3;
slippageLimit = 10;
minDLPBalance = uint256(5).mul(10 ** 18);
__Ownable_init();
__Pausable_init();
}
Auflistung 3.1: BountyManager.sol
Auswirkung Wenn sich die Offsets der oben genannten drei Funktionen ändern, funktionieren die Funktionszeiger nicht wie erwartet und die gesamte Logik des Vertrags kann geändert werden.
Vorschlag Der Vertrag sollte Schnittstellen zum Zurücksetzen der Funktionszeiger bereitstellen.
3.2 DeFi-Sicherheit
3.2.1 Potenzielles Problem 2: Unsachgemäße Berechnung des Orakels
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | In Version 11 behoben |
| Eingeführt durch | Version 1 und Version 4 |
Beschreibung Die Funktion consult() im Vertrag ComboOracle dient zur Berechnung
des Durchschnittspreises aus mehreren Quellen. In der Implementierung von
Version 1 wird das arithmetische Mittel zur Berechnung des Endpreises
verwendet, was durch Beeinflussung eines der Quellorakel manipuliert
werden kann.
function consult() public view override returns (uint256 price) {
require(sources.length != 0);
uint256 sum;
for (uint256 i = 0; i < sources.length; i++) {
uint256 price = sources[i].consult();
require(price != 0, "source consult failure");
sum = sum.add(price);
}
price = sum.div(sources.length);
}
Auflistung 3.2: ComboOracle.sol
In der Implementierung von Version 4 wird bei einem Durchschnittspreis, der größer als der niedrigste Preis × 1,025 ist, der niedrigste Preis zurückgegeben. Der Rückgabewert kann jedoch immer noch manipuliert werden, wenn der von einem der Quellorakel zurückgegebene Wert abnorm niedrig ist.
/**
* @notice Calculated price
* @return price Average price of several sources.
*/
function consult() public view override returns (uint256 price) {
require(sources.length != 0);
uint256 sum;
uint256 lowestPrice;
for (uint256 i = 0; i < sources.length; i++) {
uint256 price = sources[i].consult();
require(price != 0, "source consult failure");
if (lowestPrice == 0) {
lowestPrice = price;
} else {
lowestPrice = lowestPrice > price ? price : lowestPrice;
}
sum = sum.add(price);
}
price = sum.div(sources.length);
price = price > ((lowestPrice * 1025) / 1000) ? lowestPrice : price;
}
Auflistung 3.3: ComboOracle.sol
Auswirkung Der von ComboOracle zurückgegebene Preis kann manipuliert werden, was dem Angreifer ermöglicht, davon zu profitieren.
Vorschlag Wir empfehlen, den Medianwert anstelle des Durchschnittswerts zu verwenden. Wenn nur zwei Quellorakel vorhanden sind und ein großer Unterschied auftritt, ist es sinnvoller, die Transaktion zurückzusetzen, wenn der Durchschnittspreis deutlich höher ist als der niedrigste Preis.
Feedback Es werden nur zwei Quellorakel geben. Wenn ein großer Unterschied auftritt, werden wir einen OZ Defender Sentinel verwenden, um die zugehörigen Verträge anzuhalten.
Anmerkung Der Vertrag ComboOracle wurde entfernt und wird nicht mehr verwendet.
3.2.2 Potenzielles Problem 3: Potenzielles Abfließen von Geldern über BaseBounty
| Element | Beschreibung |
|---|---|
| Schweregrad | Hoch |
| Status | In Version 4 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Ein Benutzer kann Token (d. h. RDNT) für eine feste Dauer sperren, um
Belohnungen zu erhalten. Wenn die Sperre abläuft, können andere Benutzer
die Funktion executeBounty() aufrufen, um die Token für diesen Benutzer
wieder zu sperren, um die BaseBounty zu erhalten, wenn dieser Benutzer
AutoRelock aktiviert hat. Während des Wieder-Sperrens werden abgelaufene
Sperren gelöscht und im internen Funktionsaufruf _cleanWithdrawableLocks()
wieder in den Pool eingezahlt. Es gibt jedoch eine Variable
maxLockWithdrawPerTxn, die die maximale Anzahl der zu löschenden Sperren
begrenzt. In diesem Fall können nicht gelöschte abgelaufene Sperren
vorhanden sein, auch nachdem die Funktion executeBounty() ausgeführt wurde.
Dies kann die Prüfung in Zeile 106 der Funktion claimBounty() im Vertrag
MFDPlus weiter umgehen. issueBaseBounty wird als true gesetzt und
zurückgegeben.
**
* @notice Withdraw all lockings tokens where the unlock time has passed
*/
function _cleanWithdrawableLocks(
address user,
uint256 totalLock,
uint256 totalLockWithMultiplier
) internal returns (uint256 lockAmount, uint256 lockAmountWithMultiplier) {
LockedBalance[] storage locks = userLocks[user];
if (locks.length != 0) {
uint256 length = locks.length <= maxLockWithdrawPerTxn ? locks.length : maxLockWithdrawPerTxn;
for (uint256 i = 0; i < length; ) {
if (locks[i].unlockTime <= block.timestamp) {
lockAmount = lockAmount.add(locks[i].amount);
lockAmountWithMultiplier = lockAmountWithMultiplier.add(
locks[i].amount.mul(locks[i].multiplier)
);
locks[i] = locks[locks.length - 1];
locks.pop();
length = length - 1;
} else {
i = i + 1;
}
}
if (locks.length == 0) {
lockAmount = totalLock;
lockAmountWithMultiplier = totalLockWithMultiplier;
delete userLocks[user];
userlist.removeFromList(user);
}
}
}
Auflistung 3.4: MultiFeeDistribution.sol
Insbesondere kann der Angreifer 1 wei Token mit derselben Ablaufzeit
mehrfach sperren, was deutlich über maxLockWithdrawPerTxn liegt.
Danach kann der Angreifer die Aktion als getLpMfdBounty festlegen und
executeBounty() wiederholt aufrufen. Da die Menge der gelöschten
Sperren durch maxLockWithdrawPerTxn begrenzt ist, kann die BaseBounty
im Vertrag BountyManager vom Angreifer abgezogen werden.
Auswirkung Der Angreifer kann alle Gelder im Vertrag
BountyManager in einer einzigen Transaktion abziehen, was zur Störung
des konzipierten Bounty-Mechanismus führt.
Vorschlag Stellen Sie sicher, dass die Funktion _cleanWithdrawableLocks()
alle abgelaufenen Sperren löschen kann, und legen Sie einen Mindestbetrag
beim Sperren in der Funktion _stake() fest.
3.2.3 Potenzielles Problem 4: Mögliche ungültige Emissionspläne
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | In Version 10 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag ChefIncentivesController wird die Funktion
setEmissionSchedule() vom Besitzer aufgerufen, um Pläne für
unterschiedliche Belohnungsraten festzulegen. In diesem Fall sollte die
Startzeit für jeden Plan (_startTimeOffsets[i] + startTime) größer als
der aktuelle Zeitstempel sein. Es wird jedoch nur das erste Element in
_startTimeOffsets überprüft, was nicht ausreicht. Darüber hinaus wird
_startTimeOffsets[i] beim Hinzufügen zu emissionSchedule von uint256
in uint128 konvertiert, was abgeschnitten werden kann, wenn die
ursprüngliche Eingabe zu groß ist.
function setEmissionSchedule(
uint256[] calldata _startTimeOffsets,
uint256[] calldata _rewardsPerSecond
) external onlyOwner {
uint256 length = _startTimeOffsets.length;
require(length > 0 && length == _rewardsPerSecond.length, "empty or mismatch params");
if (startTime > 0) {
require(_startTimeOffsets[0] > block.timestamp.sub(startTime), "invalid start time");
}
for (uint256 i = 0; i < length; i++) {
emissionSchedule.push(
EmissionPoint({
startTimeOffset: uint128(_startTimeOffsets[i]),
rewardsPerSecond: uint128(_rewardsPerSecond[i])
})
);
}
emit EmissionScheduleAppended(_startTimeOffsets, _rewardsPerSecond);
}
Auflistung 3.5: ChefIncentivesController.sol
Auswirkung Wenn _startTimeOffsets nicht aufsteigend sortiert ist,
werden einige versprochene Belohnungen nicht an die Benutzer verteilt.
Wenn _startTimeOffsets[i] außerhalb des Bereichs von uint128 liegt,
wird ein ungültiger Emissionsplan hinzugefügt.
Vorschlag Stellen Sie sicher, dass _startTimeOffsets aufsteigend
sortiert ist und alle Elemente im Bereich von uint128 liegen.
3.2.4 Potenzielles Problem 5: Überspringbare Emissionspläne
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Bestätigt |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag ChefIncentivesController durchläuft die Funktion
setScheduleRewardsPerSecond() die emissionSchedule, um den Zielplan
mit dem größten Index zu finden, der bereits begonnen hat, und aktualisiert
die Belohnungsrate entsprechend. In diesem Fall können jedoch einige
Emissionspläne übersprungen werden.
function setScheduledRewardsPerSecond() internal {
if (!persistRewardsPerSecond) {
uint256 length = emissionSchedule.length;
uint256 i = emissionScheduleIndex;
uint128 offset = uint128(block.timestamp.sub(startTime));
for (; i < length && offset >= emissionSchedule[i].startTimeOffset; i++) {}
if (i > emissionScheduleIndex) {
emissionScheduleIndex = i;
_massUpdatePools();
rewardsPerSecond = uint256(emissionSchedule[i - 1].rewardsPerSecond);
}
}
}
Auflistung 3.6: ChefIncentivesController.sol
Auswirkung Wenn die Funktion setScheduledRewardsPerSecond() längere Zeit nicht
aufgerufen wird, werden einige versprochene Belohnungen möglicherweise
nicht an die Benutzer verteilt.
Vorschlag Die Funktion setScheduledRewardsPerSecond() wird innerhalb der Funktion
claim() und _handleActionAfterForToken() aufgerufen, sodass Emissionspläne nur übersprungen
werden würden, wenn keine Person während einer Emissionsperiode mit dem Protokoll interagiert.
3.2.5 Potenzielles Problem 6: Veränderbarer Wechselkurs während der Migration
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | In Version 5 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Der Vertrag Migration dient dazu, dass Benutzer von Token V1 zu Token V2
zu einem bestimmten Wechselkurs wechseln können. Während des Migrationsprozesses kann dieser
Wechselkurs jedoch vom Eigentümer über die Funktion setExchangeRate()
weiter angepasst werden.
/**
* @notice Migrate from V1 to V2
* @param amount of V1 token
*/
function exchange(uint256 amount) external whenNotPaused {
uint256 v1Decimals = tokenV1.decimals();
uint256 v2Decimals = tokenV2.decimals();
uint256 outAmount = amount.mul(1e4).div(exchangeRate).mul(10**v2Decimals).div(10**v1Decimals);
tokenV1.safeTransferFrom(_msgSender(), address(this), amount);
tokenV2.safeTransfer(_msgSender(), outAmount);
emit Migrate(_msgSender(), amount, outAmount);
}
Auflistung 3.7: Migration.sol
Auswirkung Es wäre unfair für die anderen Benutzer, wenn der Wechselkurs während des Migrationsprozesses geändert wird.
Vorschlag Sobald die Migration beginnt, sollte der Wechselkurs fest sein.
3.2.6 Potenzielles Problem 7: Unsachgemäße Implementierung von \_transfer() (I)
| Element | Beschreibung |
|---|---|
| Schweregrad | Hoch |
| Status | In Version 7 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag IncentivizedERC20 berücksichtigt die Funktion _transfer()
nicht die Situation, dass Sender und Empfänger dasselbe Konto sein können (sogenannter
Selbsttransfer). Insbesondere wenn der Sender dem Empfänger gleicht, wird der Saldo des Senders
überschrieben, wenn der Saldo des Empfängers aktualisiert wird. In diesem Fall kann der Hacker
sein eigenes Guthaben unendlich erhöhen, indem er wiederholt an sein eigenes Konto
überweist.
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual {
require(sender != address(0), 'ERC20: transfer from the zero address');
require(recipient != address(0), 'ERC20: transfer to the zero address');
_beforeTokenTransfer(sender, recipient, amount);
uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
uint256 recipientBalance = _balances[recipient].add(amount);
if (address(_getIncentivesController()) != address(0)) {
// uint256 currentTotalSupply = _totalSupply;
_getIncentivesController().handleActionBefore(sender);
if (sender != recipient) {
_getIncentivesController().handleActionBefore(recipient);
}
}
_balances[sender] = senderBalance;
_balances[recipient] = recipientBalance;
if (address(_getIncentivesController()) != address(0)) {
uint256 currentTotalSupply = _totalSupply;
_getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
if (sender != recipient) {
_getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
}
}
}
Auflistung 3.8: IncentivizedERC20.sol
Auswirkung Token können unendlich geprägt werden.
Vorschlag Implementieren Sie die Funktion _transfer() ordnungsgemäß. Zum Beispiel die Standardimplementierung
von _transfer() von ERC20 in OpenZeppelin.
_balances[sender] = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
_balances[recipient] = _balances[recipient].add(amount);
Auflistung 3.9: ERC20.sol in OpenZeppelin
3.2.7 Potenzielles Problem 8: Fehlende Prüfung des Zeitraums in UniV2TwapOracle
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | In Version 9 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag UniV2TwapOracle wird das Attribut _period in den Funktionen
initialize() und setPeriod() nicht validiert.
function initialize(
address _pair,
address _rdnt,
address _ethChainlinkFeed,
uint _period,
uint _consultLeniency,
bool _allowStaleConsults
) external initializer {
__Ownable_init();
pair = IUniswapV2Pair(_pair);
token0 = pair.token0();
token1 = pair.token1();
price0CumulativeLast = pair.price0CumulativeLast(); // Fetch the current accumulated price value (1 / 0)
price1CumulativeLast = pair.price1CumulativeLast(); // Fetch the current accumulated price value (0 / 1)
uint112 reserve0;
uint112 reserve1;
(reserve0, reserve1, blockTimestampLast) = pair.getReserves();
require(reserve0 != 0 && reserve1 != 0, 'UniswapPairOracle: NO_RESERVES'); // Ensure that there's liquidity in the pair
PERIOD = _period;
CONSULT_LENIENCY = _consultLeniency;
ALLOW_STALE_CONSULTS = _allowStaleConsults;
baseInitialize(_rdnt, _ethChainlinkFeed);
}
function setPeriod(uint _period) external onlyOwner {
PERIOD = _period;
}
Auflistung 3.10: UniV2TwapOracle.sol
Auswirkung In diesem Fall kann das Orakel einen unerwarteten Wert zurückgeben, wenn _period zu klein
ist.
Vorschlag Legen Sie eine Mindestgrenze für _period in den Funktionen initialize und setPeriod fest.
3.2.8 Potenzielles Problem 9: Nicht erstattungsfähige Kleinstbeträge
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | In Version 5 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag UniswapPoolHelper ist die Funktion zapWETH() dazu gedacht, den Benutzer beim Umtausch von WETH-Token in LP-Token zu unterstützen. Sie ruft die Funktion addLiquidityWETHOnly() auf, um Liquidität im Pool für LP-Token hinzuzufügen. Bei diesem Vorgang können Kleinstbeträge (dust tokens) entstehen, die an die Benutzer zurückgegeben werden sollten. Der UniswapPoolHelper implementiert jedoch keine solche Funktionalität, um diese Kleinstbeträge zu handhaben.
function zapWETH(uint256 amount)
public
returns (uint256 liquidity)
{
IWETH WETH = IWETH(wethAddr);
WETH.transferFrom(msg.sender, address(liquidityZap), amount);
liquidity = liquidityZap.addLiquidityWETHOnly(amount, address(this));
IERC20 lp = IERC20(lpTokenAddr);
liquidity = lp.balanceOf(address(this));
lp.safeTransfer(msg.sender, liquidity);
}
Auflistung 3.11: UniswapPoolHelper.sol
Auswirkung Die Kleinstbeträge verbleiben im Vertrag und können über die Funktion zapTokens(0,0) von anderen extrahiert werden.
Vorschlag Implementieren Sie die Funktion, um Kleinstbeträge nach dem Hinzufügen von Liquidität zurückzugeben.
3.2.9 Potenzielles Problem 10: Unsachgemäße Implementierung von \_transfer() (II)
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | In Version 9 behoben |
| Eingeführt durch | Version 7 |
Beschreibung Im Vertrag IncentivizedERC20 ruft die Funktion _transfer() die Funktion
handle_ActionAfter() auf, um den Status des Benutzers im Vertrag
ChefIncentivesController entsprechend zu aktualisieren. Der Parameter
senderBalance wird jedoch nicht aktualisiert, wenn der Sender dem Empfänger
gleicht, was falsch ist.
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual {
require(sender != address(0), 'ERC20: transfer from the zero address');
require(recipient != address(0), 'ERC20: transfer to the zero address');
_beforeTokenTransfer(sender, recipient, amount);
uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
if (address(_getIncentivesController()) != address(0)) {
// uint256 currentTotalSupply = _totalSupply;
_getIncentivesController().handleActionBefore(sender);
if (sender != recipient) {
_getIncentivesController().handleActionBefore(recipient);
}
}
_balances[sender] = senderBalance;
uint256 recipientBalance = _balances[recipient].add(amount);
_balances[recipient] = recipientBalance;
if (address(_getIncentivesController()) != address(0)) {
uint256 currentTotalSupply = _totalSupply;
_getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
if (sender != recipient) {
_getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
}
}
}
Auflistung 3.12: IncentivizedERC20.sol
Auswirkung Wenn Benutzer an sich selbst überweisen, wird ihr Zustand im Vertrag
ChefIncentivesController nicht ordnungsgemäß aktualisiert, was zu weiteren Problemen bei den
Belohnungen führt.
Vorschlag Korrigieren Sie senderBalance in der Funktion handleActionAfter().
3.2.10 Potenzielles Problem 11: Manipulierbare Compound-Belohnungen
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | In Version 10 behoben |
| Eingeführt durch | Version 5 |
Beschreibung Im Vertrag MFDPlus tauscht die Funktion _convertPendingRewardsToWeth()
die Belohnungen des Benutzers über den Uniswap-Router gegen WETH zum erneuten Sperren. Es fehlt jedoch
eine Slippage-Prüfung nach dem Tausch.
IERC20(underlying).safeApprove(uniRouter, removedAmount);
uint256[] memory amounts = IUniswapV2Router02(uniRouter)
.swapExactTokensForTokens(
removedAmount,
0, // slippage handled after this function
mfdHelper.getRewardToBaseRoute(underlying),
address(this),
block.timestamp + 10
);
Auflistung 3.13: MFDPlus.sol
Auswirkung Der Angreifer kann die Transaktion front-runnen, um den Preis zu manipulieren und den Gewinn zu erzielen.
Vorschlag Fügen Sie die Slippage-Prüfung in der Funktion claimCompound() hinzu.
3.2.11 Potenzielles Problem 12: Fehlende Zugriffskontrolle in setLeverager()
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | In Version 9 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion setLeverager() im Vertrag LendingPool hat keine
Zugriffskontrolle.
uint256[] memory amounts = IUniswapV2Router02(uniRouter)
.swapExactTokensForTokens(
removedAmount,
0, // slippage handled after this function
mfdHelper.getRewardToBaseRoute(underlying),
address(this),
block.timestamp + 10
);
Auflistung 3.14: LendingPool.sol
Auswirkung Wenn der Leverager nicht von Anfang an gesetzt wurde, könnte ein
Angreifer den Leverager auf eine beliebige Adresse setzen und somit die Kontrolle über die
Logik der Funktion depositWithAutoDLP() erlangen.
Vorschlag Setzen Sie den Leverager in der Funktion initialize() oder fügen Sie die
Zugriffskontrolle für die Funktion setLeverager() hinzu.
3.2.12 Potenzielles Problem 13: Keine Slippage-Prüfung in addLiquidityWETHOnly()
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | Bestätigt |
| Eingeführt durch | Version 1 |
Beschreibung Der Benutzer kann entweder geliehene WETH-Token (oder seine eigenen ETH-Token) oder Vesting-RDNT-Token in MFD-Verträgen verwenden, um LP-Token (d. h. WETH-RDNT) zu erhalten.
Bei der Hinzufügung von Liquidität zum Pool basiert die Berechnung der
erforderlichen Token jedoch auf der Menge der Reserven im Pool, was
manipuliert werden kann. In diesem Fall, wenn der Benutzer nur WETH-Token
hat, wird die Funktion addLiquidityWETHOnly() aufgerufen, um die Hälfte der
WETH-Token im unausgeglichenen Pool ohne Slippage-Prüfung gegen RDNT-Token zu
tauschen.
function addLiquidityWETHOnly(uint256 _amount, address payable to)
public
returns (uint256 liquidity)
{
require(to != address(0), "LiquidityZAP: Invalid address");
uint256 buyAmount = _amount.div(2);
require(buyAmount > 0, "LiquidityZAP: Insufficient ETH amount");
(uint256 reserveWeth, uint256 reserveTokens) = getPairReserves();
uint256 outTokens = UniswapV2Library.getAmountOut(
buyAmount,
reserveWeth,
reserveTokens
);
_WETH.transfer(_tokenWETHPair, buyAmount);
(address token0, address token1) = UniswapV2Library.sortTokens(
address(_WETH),
_token
);
IUniswapV2Pair(_tokenWETHPair).swap(
_token == token0 ? outTokens : 0,
_token == token1 ? outTokens : 0,
address(this),
""
);
return _addLiquidity(outTokens, buyAmount, to);
}
Auflistung 3.15: LiquidityZap.sol
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
Auflistung 3.16: UniswapV2Library.sol
Auswirkung Der Angreifer kann die Transaktion front-runnen, um den Preis zu manipulieren und den Gewinn zu erzielen.
Vorschlag Überprüfen Sie die Slippage in der Funktion addLiquidityWETHOnly()
oder stellen Sie sicher, dass sie nur vom UniswapPoolHelper aufgerufen werden kann.
3.2.13 Potenzielles Problem 14: Fehlende Prüfung des borrowRatio in loopETH()
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | In Version 10 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion loopETH() wird für die Hebelwirkung beim Leihen verwendet und
erhält einen Parameter borrowRatio, um das Leihverhältnis anzugeben. Das
borrowRatio wird jedoch vor Beginn der Schleife nicht geprüft.
function loopETH(
uint256 interestRateMode,
uint256 borrowRatio,
uint256 loopCount
) external payable {
uint16 referralCode = 0;
uint256 amount = msg.value;
if (IERC20(address(weth)).allowance(address(this), address(lendingPool)) == 0) {
IERC20(address(weth)).safeApprove(address(lendingPool), type(uint256).max);
}
if (IERC20(address(weth)).allowance(address(this), address(treasury)) == 0) {
IERC20(address(weth)).safeApprove(treasury, type(uint256).max);
}
uint256 fee = amount.mul(feePercent).div(RATIO_DIVISOR);
_safeTransferETH(treasury, fee);
amount = amount.sub(fee);
weth.deposit{value: amount}();
lendingPool.deposit(address(weth), amount, msg.sender, referralCode);
for (uint256 i = 0; i < loopCount; i += 1) {
amount = amount.mul(borrowRatio).div(RATIO_DIVISOR);
lendingPool.borrow(address(weth), amount, interestRateMode, referralCode, msg.sender);
weth.withdraw(amount);
fee = amount.mul(feePercent).div(RATIO_DIVISOR);
_safeTransferETH(treasury, fee);
weth.deposit{value: amount.sub(fee)}();
lendingPool.deposit(address(weth), amount.sub(fee), msg.sender, referralCode);
}
zapWETHWithBorrow(wethToZap(msg.sender), msg.sender);
}
Auflistung 3.17: Leverager.sol
Auswirkung Das borrowRatio kann höher sein als RATIO_DIVISOR, was nicht mit dem ursprünglichen
Design übereinstimmt.
Vorschlag Stellen Sie sicher, dass borrowRatio kleiner oder gleich RATIO_DIVISOR ist.
3.2.14 Potenzielles Problem 15: Fehlende Prüfung der Länge zwischen assets und poolIDs in setPoolIDs()
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | In Version 10 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion setPoolIDs() ermöglicht es dem Eigentümer, verschiedene
poolIDs für verschiedene Assets festzulegen. Die Längen dieser beiden
Arrays werden jedoch nicht auf Gleichheit geprüft.
// Set pool ids of assets
function setPoolIDs(address[] memory assets, uint256[] memory poolIDs) external onlyOwner {
for (uint256 i = 0; i < assets.length; i += 1) {
poolIdPerChain[assets[i]] = poolIDs[i];
}
emit PoolIDsUpdated(assets, poolIDs);
}
Auflistung 3.18: StarBorrow.sol
Auswirkung Die Assets werden nicht den richtigen poolIDs zugewiesen.
Vorschlag Stellen Sie sicher, dass die Längen der Assets und poolIDs gleich sind.
3.2.15 Potenzielles Problem 16: Fehlende Überprüfung des mint-Privilegs bei addBountyContract()
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Bestätigt |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion addBountyContract() dient zur Festlegung des neuen
BountyManager. Der ursprüngliche Bounty-Vertrag behält jedoch das
Mint-Privileg, was gegen das ursprüngliche Design verstößt.
function addBountyContract(address _bounty) external onlyOwner {
BountyManager = _bounty;
minters[_bounty] = true;
}
Auflistung 3.19: Leverager.sol
Auswirkung Der veraltete BountyManager hat weiterhin Mint-Privilegien.
Vorschlag Widerrufen Sie die Mint-Privilegien des ursprünglichen BountyManager-Vertrags.
Feedback Die Funktion addBountyContract wird nur einmal aufgerufen, um den BountyManager
zu initialisieren.
3.2.16 Potenzielles Problem 17: Minters können nur einmal zugewiesen werden
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Bestätigt |
| Eingeführt durch | Version 1 |
Beschreibung Die minters werden verwendet, um diejenigen zu erfassen, die die Berechtigung
haben, auf die Funktionen mint() und addReward() zuzugreifen. Wenn jedoch einer der
minters (z. B. der Vertrag ChefIncentivesController) aktualisiert wird, können die veralteten
minters nicht entfernt werden.
function setMinters(address[] memory _minters) external onlyOwner {
require(!mintersAreSet);
for (uint256 i; i < _minters.length; i++) {
minters[_minters[i]] = true;
}
mintersAreSet = true;
}
Auflistung 3.20: MultiFeeDistribution.sol
Auswirkung Die veralteten minters können nicht entfernt werden, wenn sie aktualisiert werden.
Vorschlag Implementieren Sie eine privilegierte Funktion zur Änderung von minters.
Feedback Da BountyManager, ChefIncentivesController und MultiFeeDistribution
aktualisierbar sein werden, bleiben die minters immer mit der gleichen Proxy-Adresse
verbunden.
3.3 Zusätzliche Empfehlungen
3.3.1 Potenzielles Problem 18: Gasoptimierung (zapVestingToLp() in Mfd)
| Element | Beschreibung |
|---|---|
| Status | In Version 10 behoben |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion zapVestingToLp() kann nur vom Vertrag LockZap aufgerufen werden,
um die gesperrten Einnahmen des Benutzers zu transferieren. Sie durchläuft das Einnahmen-Array des
Benutzers ab Index 0 und prüft, ob unlockTime größer ist als der aktuelle Zeitstempel. Wenn ja, wird diese
Einnahme aus dem Array entfernt und transferiert. Da unlockTime im Array mit dem Index steigt, ist es
jedoch effizienter, die Iteration vom Ende des Arrays zum Anfang zu starten. Wenn unlockTime kleiner
ist als der aktuelle Zeitstempel, kann die Schleife unterbrochen werden.
function zapVestingToLp(address _user)
external
override
returns (uint256 zapped)
{
require(msg.sender == lockZap);
LockedBalance[] storage earnings = userEarnings[_user];
uint256 length = earnings.length;
for (uint256 i = 0; i < length; ) {
// only vesting, so only look at currently locked items
if (earnings[i].unlockTime > block.timestamp) {
zapped = zapped.add(earnings[i].amount);
// remove + shift array size
earnings[i] = earnings[earnings.length - 1];
earnings.pop();
length = length.sub(1);
} else {
i = i.add(1);
}
}
rdntToken.safeTransfer(lockZap, zapped);
Balances storage bal = balances[_user];
bal.earned = bal.earned.sub(zapped);
bal.total = bal.total.sub(zapped);
return zapped;
}
Auflistung 3.21: MultiFeeDistribution.sol
Vorschlag Beginnen Sie die Iteration vom Ende der Einnahmen zum Anfang. Wenn unlockTime
kleiner ist als der aktuelle Zeitstempel, kann die Schleife unterbrochen werden.
3.3.2 Potenzielles Problem 19: Nicht-leere Bounty-Reserve in BountyManager
| Element | Beschreibung |
|---|---|
| Status | In Version 10 behoben |
| Eingeführt durch | Version 1 |
Beschreibung In der Funktion _sendBounty() wird, wenn nicht genügend RDNT-Token für die
Überweisung im Vertrag BountyManager vorhanden sind, das Ereignis
BountyReserveEmpty() ausgelöst und der Vertrag angehalten. Es ist jedoch
möglich, dass noch einige RDNT-Token übrig sind, was im Widerspruch zu
dem ausgelösten Ereignis steht.
function _sendBounty(address _to, uint256 _amount)
internal
returns (uint256)
{
if (_amount == 0) {
return 0;
}
uint256 bountyReserve = IERC20(rdnt).balanceOf(address(this));
if(_amount > bountyReserve) {
emit BountyReserveEmpty(bountyReserve);
_pause();
} else {
IERC20(rdnt).safeTransfer(address(mfd), _amount);
IMFDPlus(mfd).mint(_to, _amount, true);
return _amount;
}
}
Auflistung 3.22: BountyManager.sol
Vorschlag Überweisen Sie die verbleibenden RDNT-Token aus, auch wenn es nicht genug ist.
3.3.3 Potenzielles Problem 20: Inkonsistente Benennung in requiredUsdValue()
| Element | Beschreibung |
|---|---|
| Status | Bestätigt |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion requiredUsdValue() dient zur Überprüfung des erforderlichen
gesperrten Wertes des Benutzers, der für die Prämienberechtigung durch das Halten von RTokens
qualifiziert ist. Die Berechnung basiert auf dem Beleihungswert des Benutzers, der von der Funktion
getUserAccountData() zurückgegeben wird. Der zurückgegebene Wert wird jedoch als
totalCollateralETH bezeichnet, was inkonsistent mit dem in der Funktion
requiredUsdValue() (d. h. totalCollateralUSD) ist.
Vorschlag Standardisieren Sie die Benennung von Funktionen mit dem richtigen Tokennamen. Benennen Sie
zum Beispiel requiredUsdValue() in requiredEthValue() um.
Feedback Wir möchten die AAVE-Verträge so ähnlich wie möglich halten, daher haben wir den Namen nicht geändert.
3.4 Anmerkungen
3.4.1 Potenzielles Problem 21: Veraltete MFDPlus
| Element | Beschreibung |
|---|---|
| Status | Bestätigt |
| Eingeführt durch | Version 10 |
Beschreibung Der Vertrag MFDPlus wird nicht mehr verwendet. Die Logik des Compoundings wurde
in den Vertrag AutoCompounder verlagert und andere Logik in den Vertrag
MiddleFeeDistribution.
4. Anhang
4.1 Ergebnisse der automatisierten statischen Sicherheitstests
Tabelle 4.1: Ergebnisse der automatisierten statischen Sicherheitstests. Found gibt die Anzahl der von den Tools gemeldeten Probleme an. FP steht für die Anzahl der falsch positiven Ergebnisse nach unserer manuellen Überprüfung.
| ID | Detektor | Beschreibung | Auswirkung | Gefunden | FP | Ergebnis |
|---|---|---|---|---|---|---|
| 1 | arbitrary-send-erc20 | Aufruf von transferFrom mit beliebiger von-Adresse | Hoch | 1 | 1 | Bestanden |
| 2 | array-by-reference | Speicherauszug ändern nach Wert | Hoch | 0 | 0 | Bestanden |
| 3 | incorrect-shift | Falsche Reihenfolge der Parameter in einer Shift-Anweisung | Hoch | 0 | 0 | Bestanden |
| 4 | multiple-constructors | Mehrere Konstruktorschemata | Hoch | 0 | 0 | Bestanden |
| 5 | name-reused | Wiederverwendung des Vertragsnamens | Hoch | 0 | 0 | Bestanden |
| 6 | protected-vars | Variablen direkt ohne Zugriffskontrolle ändern | Hoch | 0 | 0 | Bestanden |
| 7 | rtlo | Verwendung von Rechts-nach-Links-Override-Steuerzeichen | Hoch | 0 | 0 | Bestanden |
| 8 | shadowing-state | Zustandsvariablen überschatten | Hoch | 1 | 1 | Bestanden |
| 9 | suicidal | Funktionen, die es jedem erlauben, den Vertrag zu zerstören | Hoch | 0 | 0 | Bestanden |
| 10 | uninitialized-state | Uninitialisierte Zustandsvariablen | Hoch | 3 | 3 | Bestanden |
| 11 | uninitialized-storage | Uninitialisierte Speicher-Variablen | Hoch | 0 | 0 | Bestanden |
| 12 | unprotected-upgrade | Ungeschützter aktualisierbarer Vertrag | Hoch | 1 | 1 | Bestanden |
| 13 | arbitrary-send-erc20-permit | transferFrom verwendet beliebige von-Adresse mit Permit | Hoch | 0 | 0 | Bestanden |
| 14 | arbitrary-send-eth | Funktionen, die Ether an beliebige Ziele senden | Hoch | 0 | 0 | Bestanden |
| 15 | controlled-array-length | Kontrollierte Array-Längenzuweisung | Hoch | 0 | 0 | Bestanden |
| 16 | controlled-delegatecall | Kontrolliertes delegatecall-Ziel | Hoch | 0 | 0 | Bestanden |
| 17 | delegatecall-loop | Zahlbare Funktionen, die delegatecall innerhalb einer Schleife verwenden | Hoch | 0 | 0 | Bestanden |
| 18 | msg-value-loop | msg.value in einer Schleife verwenden | Hoch | 0 | 0 | Bestanden |
| 19 | reentrancy-eth | Reentrancy-Schwachstellen (Diebstahl von Ether) | Hoch | 5 | 5 | Bestanden |
| 20 | storage-array | Compiler-Fehler bei signed storage integer array | Hoch | 0 | 0 | Bestanden |
| 21 | unchecked-transfer | Ungeprüfte Token-Übertragung | Hoch | 12 | 12 | Bestanden |
| 22 | weak-prng | Schwacher PRNG | Hoch | 0 | 0 | Bestanden |
| 23 | domain-separator-collision | Erkennt ERC20-Token, die eine Funktion mit einer Signatur haben, die mit EIP-2612s DOMAIN_SEPARATOR() kollidiert | Mittel | 0 | 0 | Bestanden |
| 24 | enum-conversion | Erkennt gefährliche Enum-Konvertierungen | Mittel | 0 | 0 | Bestanden |
| 25 | erc20-interface | Falsche ERC20-Schnittstellen | Mittel | 0 | 0 | Bestanden |
| 26 | erc721-interface | Falsche ERC721-Schnittstellen | Mittel | 0 | 0 | Bestanden |
| 27 | incorrect-equality | Gefährliche strikte Gleichheiten | Mittel | 23 | 23 | Bestanden |
| 28 | locked-ether | Verträge, die Ether sperren | Mittel | 1 | 1 | Bestanden |
| 29 | mapping-deletion | Löschen auf Mapping, das eine Struktur enthält | Mittel | 0 | 0 | Bestanden |
| 30 | shadowing-abstract | Zustandsvariablen überschatten abstrakte Verträge | Mittel | 0 | 0 | Bestanden |
| 31 | tautology | Tautologie oder Widerspruch | Mittel | 0 | 0 | Bestanden |
| 32 | write-after-write | Ungenutzte Schreiboperation | Mittel | 3 | 3 | Bestanden |
| 33 | boolean-cst | Missbrauch von Boolescher Konstante | Mittel | 0 | 0 | Bestanden |
| 34 | constant-function-asm | Konstante Funktionen mit Assembly-Code | Mittel | 0 | 0 | Bestanden |
| 35 | constant-function-state | Konstante Funktionen, die den Zustand ändern | Mittel | 0 | 0 | Bestanden |
| 36 | divide-before-multiply | Ungenaue arithmetische Operationenreihenfolge | Mittel | 20 | 20 | Bestanden |
| 37 | reentrancy-no-eth | Reentrancy-Schwachstellen (kein Diebstahl von Ether) | Mittel | 12 | 12 | Bestanden |
| 38 | reused-constructor | Wiederverwendeter Basis-Konstruktor | Mittel | 0 | 0 | Bestanden |
| 39 | tx-origin | Gefährliche Verwendung von tx.origin | Mittel | 1 | 1 | Bestanden |
| 40 | unchecked-lowlevel | Ungeprüfte Low-Level-Aufrufe | Mittel | 0 | 0 | Bestanden |
| 41 | unchecked-send | Ungeprüfte Sendung | Mittel | 0 | 0 | Bestanden |
| 42 | uninitialized-local | Uninitialisierte lokale Variablen | Mittel | 33 | 33 | Bestanden |
| 43 | unused-return | Ungenutzte Rückgabewerte | Mittel | 19 | 19 | Bestanden |
4.2 Ergebnisse der automatisierten dynamischen Sicherheitstests
Tabelle 4.2: Getestete Eigenschaften für die Lending-bezogene Logik
| ID | Eigenschaft | Ergebnis |
|---|---|---|
| 1 | Aufruf von deposit führt nie zu einer Verringerung des RToken-Betrags von onBehalfOf | Bestanden |
| 2 | Aufruf von withdraw führt nie zu einer Erhöhung des RToken-Betrags von msg.sender | Bestanden |
| 3 | Aufruf von borrow mit stabilem Zinssatzmodus führt nie zu einer Verringerung von onBehalfOfs StableDebtToken | Bestanden |
| 4 | Aufruf von borrow mit variablem Zinssatzmodus führt nie zu einer Verringerung von onBehalfOfs VariableDebtToken | Bestanden |
| 5 | Aufruf von borrow mit onBehalfOf, das nicht msg.sender entspricht, führt nie zu einer Erhöhung der Kreditgenehmigung von msg.sender | Bestanden |
| 6 | Aufruf von repay mit stabilem Zinssatzmodus führt nie zu einer Erhöhung von onBehalfOfs StableDebtToken | Bestanden |
| 7 | Aufruf von repay mit variablem Zinssatzmodus führt nie zu einer Erhöhung von onBehalfOfs VariableDebtToken | Bestanden |
| 8 | liquidityIndex nimmt nie ab | Bestanden |
| 9 | liquidityIndex bleibt innerhalb desselben Blocks konstant | Bestanden |
| 10 | variableBorrowIndex nimmt nie ab | Bestanden |
| 11 | variableBorrowIndex bleibt innerhalb desselben Blocks konstant | Bestanden |
| 12 | Die Verringerung der Sicherheitenbeträge führt nie zu einem Gesundheitsfaktor unter 1 | Bestanden |
| 13 | Die Erhöhung der Kreditbeträge führt nie zu einem Gesundheitsfaktor unter 1 | Bestanden |
Tabelle 4.3: Getestete Eigenschaften für die Staking-bezogene Logik
| ID | Eigenschaft | Ergebnis |
|---|---|---|
| 1 | Das Gesamtguthaben des Benutzers entspricht immer der Summe aus gesperrtem Guthaben, ungesperrtem Guthaben und verdienten Guthaben | Bestanden |
| 2 | Das gesperrte Guthaben des Benutzers entspricht immer der Summe des gesperrten Guthabens | Bestanden |
| 3 | Das gesperrte Guthaben mit Multiplikator des Benutzers entspricht immer der Summe des gesperrten Guthabens mal dem Multiplikator des gesperrten Guthabens | Bestanden |
| 4 | lockedSupply entspricht immer der Summe der gesperrten Guthaben der Benutzer | Bestanden |
| 5 | lockedSupplyWithMultiplier entspricht immer der Summe der gesperrten Guthaben mit Multiplikator der Benutzer | Bestanden |
| 6 | rewardPerTokenStored nimmt nie ab | Bestanden |
| 7 | rewardPerTokenStored bleibt innerhalb desselben Blocks konstant | Bestanden |
| 8 | totalSupply entspricht immer der Summe der Beträge der Benutzer | Bestanden |
| 9 | accRewardPerShare nimmt nie ab | Bestanden |
| 10 | accRewardPerShare bleibt innerhalb desselben Blocks konstant | Bestanden |
Tabelle 4.4: Getestete Eigenschaften für andere Funktionen
| ID | Eigenschaft | Ergebnis |
|---|---|---|
| 1 | WETH- und RDNT-Guthaben des Vertrags LockedZap sind immer null | Bestanden |
| 2 | WETH- und RDNT-Guthaben des Vertrags LiquidityZap sind immer null | Bestanden |
| 3 | WETH- und RDNT-Guthaben des Vertrags BalancerPoolHelper sind immer null | Bestanden |
| 4 | WETH- und RDNT-Guthaben des Vertrags UniswapPoolHelper sind immer null | Bestanden |
| 5 | Aufruf von loop führt immer dazu, dass der Benutzer für Belohnungen berechtigt ist | Bestanden |
| 6 | Aufruf von loopETH führt immer dazu, dass der Benutzer für Belohnungen berechtigt ist | Bestanden |
| 7 | Aufruf von executeBounty mit _execute gleich false führt nie zu einer Speicheränderung |
Bestanden |
| 8 | Aufruf von transfer mit sender gleich receiver führt nie zu einer Guthabenänderung | In Version 1 fehlgeschlagen. In Version 7 bestanden |
5. Hinweise und Bemerkungen
5.1 Haftungsausschluss
Dieser Bericht stellt keine Anlageberatung oder persönliche Empfehlung dar. Er berücksichtigt nicht die potenzielle Wirtschaftlichkeit eines Tokens, eines Token-Verkaufs oder eines anderen Produkts, einer Dienstleistung oder eines anderen Vermögenswerts und sollte auch nicht als solche interpretiert werden. Kein Unternehmen sollte sich in irgendeiner Weise auf diesen Bericht verlassen, einschließlich der Entscheidungsfindung zum Kauf oder Verkauf von Token, Produkten, Dienstleistungen oder anderen Vermögenswerten.
Dieser Bericht ist keine Befürwortung eines bestimmten Projekts oder Teams, und der Bericht garantiert nicht die Sicherheit eines bestimmten Projekts. Diese Sicherheitstests geben keine Gewährleistung für die Entdeckung aller Sicherheitsprobleme der Smart Contracts, d. h. das Ergebnis der Bewertung garantiert nicht die Nichtexistenz weiterer Sicherheitsprobleme. Da die Sicherheitstests nicht als umfassend angesehen werden können, empfehlen wir stets die Durchführung unabhängiger Audits und eines öffentlichen Bug-Bounty-Programms, um die Sicherheit von Smart Contracts zu gewährleisten.
Der Umfang dieser Sicherheitstests beschränkt sich auf den in Abschnitt 1.2 genannten Code. Sofern nicht ausdrücklich anders angegeben, liegt die Sicherheit der Sprache selbst (z. B. der Solidity-Sprache), der zugrunde liegenden Compiling-Toolchain und der Computing-Infrastruktur außerhalb des Rahmens.
5.2 Vorgehensweise bei der Prüfung
Wir führen die Prüfung gemäß folgendem Verfahren durch.
-
Schwachstellenerkennung Wir scannen zunächst Smart Contracts mit automatischen Code-Analysatoren und überprüfen dann manuell (lehnen ab oder bestätigen) die von ihnen gemeldeten Probleme.
-
Semantische Analyse Wir untersuchen die Geschäftslogik von Smart Contracts und führen weitere Untersuchungen zu möglichen Schwachstellen mithilfe eines automatischen Fuzzing-Tools (entwickelt von unserem Forschungsteam) durch. Wir analysieren auch manuell mögliche Angriffsszenarien mit unabhängigen Prüfern, um das Ergebnis zu kreuzprüfen.
-
Empfehlung Wir geben Entwicklern nützliche Ratschläge aus der Perspektive guter Programmierpraktiken, einschließlich Gasoptimierung, Codestil usw.
Wir zeigen die wichtigsten konkreten Prüfpunkte im Folgenden.
5.2.1 Software-Sicherheit
-
Reentrancy
-
DoS
-
Zugriffskontrolle
-
Datenverarbeitung und Datenfluss
-
Fehlerbehandlung
-
Nicht vertrauenswürdige externe Aufrufe und Kontrollfluss
-
Konsistenz der Initialisierung
-
Ereignisoperationen
-
Fehleranfällige Zufälligkeit
-
Unsachgemäße Verwendung des Proxy-Systems
5.2.2 DeFi-Sicherheit
-
Semantische Konsistenz
-
Funktionale Konsistenz
-
Berechtigungsverwaltung
-
Geschäftslogik
-
Tokenoperation
-
Notfallmechanismus
-
Orakelsicherheit
-
Whitelist und Blacklist
-
Wirtschaftliche Auswirkungen
-
Batch-Übertragung
5.2.3 NFT-Sicherheit
-
Duplizierte Artikel
-
Verifizierung des Token-Empfängers
-
Off-Chain-Metadatensicherheit
5.2.4 Zusätzliche Empfehlung
-
Gasoptimierung
-
Codequalität und -stil
Hinweis: Die vorherigen Checkpoints sind die wichtigsten. Wir verwenden möglicherweise weitere Checkpoints während des Auditprozesses, abhängig von der Funktionalität des Projekts.



