Berichtsübersicht
| 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. Einführung
1.1 Über Sicherheitstests
Wir wurden von Radiant Capital eingeladen, einen Sicherheitstest (als Red Team) für die Smart Contracts von Radiant V2 durchzuführen, um potenzielle Risiken zu identifizieren. Als verantwortungsvolles Team nimmt Radiant Capital Sicherheit ernst. Daher hat das Team beschlossen, mehr Aufwand in die Absicherung dieser Smart Contracts zu investieren, obwohl diese bereits von mehreren Sicherheitsunternehmen geprüft wurden ^1.
Zu beachten ist, dass sich Sicherheitstests von Sicherheitsaudits sowohl in Zielen als auch in Anforderungen unterscheiden. Insbesondere zielt der Sicherheitstest darauf ab, zusätzliche/ungewöhnliche verwundbare Punkte zu entdecken, indem Angreifer nachgeahmt werden, um das Programm/Protokoll zu kompromittieren, während ein Sicherheitsaudit darauf abzielt, eine relativ umfassende Sicherheitsprüfung durch Aufzählung der möglichen Angriffs-oberflächen durchzuführen. Daher ist es möglich, dass Sicherheitstests einige komplizierte Logikfehler nicht abdecken können, die durch einen Sicherheitsaudit aufgrund der begrenzten Zeit und Ressourcen identifiziert werden könnten.
1.2 Über die Zielverträge
| Information | Beschreibung |
|---|---|
| Typ | Smart Contract |
| Sprache | Solidity |
| Ansatz | Statische Analyse, dynamische Analyse, halbautomatische und manuelle Überprüfung |
Das Ziel-Repository ist Radiant_v2.1.1. Die Commit-SHA-Werte während der Sicherheitstests sind im Folgenden dargestellt. Unser Bericht ist verantwortlich für die anfängliche Version (d.h. Version 1) sowie für neuen Code zur Behebung von Problemen im Bericht.
Zu beachten ist, dass dieser Bericht nur Smart Contracts im radiant_v2.1.1/contracts-Ordner dieses Repositories abdeckt, einschließlich:
- bounties
- deployments
- flashloan
- leverage
- lock
- oracles
- staking
- zap
- eligibility
- misc
- oft
- protocol
- stargate
Nach der Aktualisierung in Version 8 umfassen die in diesem Sicherheitstest 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 Empfehlungen, die sowohl von der Industrie als auch von der Wissenschaft weit verbreitet angewendet werden, einschließlich der OWASP-Risikobewertungsmethodik ^2 und der Common Weakness Enumeration ^3. Der Schweregrad des Risikos wird durch Wahrscheinlichkeit und Auswirkung bestimmt. Insbesondere wird die Wahrscheinlichkeit verwendet, um abzuschätzen, wie wahrscheinlich es ist, dass eine bestimmte Sicherheitslücke von einem Angreifer entdeckt und ausgenutzt werden kann, während die Auswirkung verwendet wird, um die Konsequenzen einer erfolgreichen Ausnutzung zu messen.
In diesem Bericht werden sowohl Wahrscheinlichkeit als auch Auswirkung in zwei Bewertungen eingeteilt, d.h. hoch und niedrig, und ihre Kombinationen sind in Tabelle 1.1 dargestellt.
Dementsprechend werden die in diesem Bericht gemessenen Schweregrade in drei Kategorien eingeteilt: 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 Rückmeldung.
-
Zur Kenntnis genommen Das Element wurde vom Kunden empfangen, 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 hauseigenes statisches Analysetool auf Basis von Slither, um das Vorhandensein von Sicherheitslücken zu überprüfen. Nach manueller Überprüfung der Ergebnisse wurden keine Probleme gefunden. Detaillierte Testergebnisse sind in Tabelle 4.1 im Anhang zu finden.
2.2 Automatisierte dynamische Sicherheitstests
Wir nutzen Fuzzing-Techniken, um die Robustheit, Zuverlässigkeit und Präzision der Zielverträge zu testen. Der anfängliche Seed im Fuzzing-Prozess wird basierend auf der Funktionssemantik und Vertragstestskripten bestimmt. Um die On-Chain-Umgebung zu simulieren, pflegen wir auch einen Satz von Adressen, die mit den Verträgen LendingPool und MultiFeeDistribution interagiert haben.
Unser Fuzzer berücksichtigt auch die Funktionssemantik bei der Transaktionssequenz- generierung. Zum Beispiel werden die Funktion stake im Vertrag MultiFeeDistribution und die Funktion deposit im Vertrag LendingPool wahrscheinlich zuerst in der Sequenz aufgerufen. Die Mutation der Funktionsparameter und -sequenz wird durch die Vertragscode- abdeckung gesteuert. Wenn ein bestimmter Parameter oder eine Sequenz eine höhere Code- abdeckung erreicht, hat er/sie eine höhere Priorität, in der nächsten Fuzzing- Runde mutiert zu werden. Um einige durch Magic Numbers eingeschränkte Pfade zu erkunden, sammeln wir die aus dem Speicher gelesenen Werte (d.h. die SLOAD-Anweisung) zur Laufzeit und verwenden sie zur Generierung von Funktionsparametern während des Mutationsprozesses.
Insgesamt generieren wir 100.000 Testfälle und nutzen 31 Orakel, die verwendet werden, um zu erkennen, ob ein Fehler aufgetreten ist. Jeder Testfall enthält 30 Transaktionen mit festgelegten Reihenfolgen. Schließlich entdeckten wir ein kritisches Problem (d.h. Abschnitt 3.2.6), das auch in unserem manuellen Sicherheitstestprozess entdeckt wurde. Detaillierte Testergebnisse sind in Tabellen 4.2, 4.3 und 4.4 im Anhang zu finden.
3. Manuelle Sicherheitstests
Wir setzen manuelle Bemühungen ein, um das Gesamtdesign und die Interaktionen zwischen verschiedenen Modulen zu verstehen, und führen dann die Sicherheitstests basierend auf unserem Know-how über potenzielle Angriffsoberflä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 einen Hinweis wie folgt:
-
Hohes Risiko: 2
-
Mittleres Risiko: 8
-
Niedriges Risiko: 7
-
Empfehlungen: 3
-
Hinweise: 1
| ID | Schweregrad | Beschreibung | Kategorie | Status |
|---|---|---|---|---|
| 1 | Mittel | Keine reservierte Schnittstelle zum Zurücksetzen von Funktionszeigern | Software-Sicherheit | Behoben |
| 2 | Mittel | Fehlerhafte Berechnung des Orakels | DeFi-Sicherheit | Behoben |
| 3 | Hoch | Potenzieller Mittelabfluss durch BaseBounty | DeFi-Sicherheit | Behoben |
| 4 | Niedrig | Potentiell 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 | Fehlerhafte Implementierung von _transfer() (I) | DeFi-Sicherheit | Behoben |
| 8 | Niedrig | Fehlende Überprüfung der Periode in UniV2TwapOracle | DeFi-Sicherheit | Behoben |
| 9 | Mittel | Nicht rückerstattbare Staub-Token | DeFi-Sicherheit | Behoben |
| 10 | Mittel | Fehlerhafte 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 Überprüfung von borrowRatio in loopETH() | DeFi-Sicherheit | Behoben |
| 15 | Niedrig | Fehlende Längenüberprüfung zwischen assets und poolIDs in setPoolIDs() | DeFi-Sicherheit | Behoben |
| 16 | Niedrig | Fehlende Widerrufung des mint-Privilegs in addBountyContract() | DeFi-Sicherheit | Bestätigt |
| 17 | Niedrig | Minter können nur einmal zugewiesen werden | DeFi-Sicherheit | Bestätigt |
| 18 | - | Gas-Optimierung (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-Notiz | Hinweis | Bestätigt |
Die Details werden in den folgenden Abschnitten bereitgestellt.
3.1 Software-Sicherheit
3.1.1 Potenzielles Problem 1: Keine reservierte Schnittstelle zum Zurücksetzen von Funktionszeigern
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | Behoben in Version 7 |
| Eingeführt durch | Version 1 |
Beschreibung Drei Funktionen, getLpMfdBounty(), getChefBounty() und getAutoCompoundBounty(), werden über Funktionszeiger im Vertrag BountyManager aufgerufen. Gleichzeitig zeigt die Vererbung von OwnableUpgradable, dass dieser Vertrag die Implementierung eines Proxys sein würde. Dies zeigt, dass der Implementierungsvertrag in der 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();
}
Listing 3.1: BountyManager.sol
Auswirkung Wenn die Offsets der oben genannten drei Funktionen geändert werden, können die Funktionszeiger nicht wie erwartet funktionieren und die gesamte Logik des Vertrags kann geändert werden.
Empfehlung Der Vertrag sollte Schnittstellen zum Zurücksetzen der Funktionszeiger bereitstellen.
3.2 DeFi-Sicherheit
3.2.1 Potenzielles Problem 2: Fehlerhafte Berechnung des Orakels
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | Behoben in Version 11 |
| Eingeführt durch | Version 1 und Version 4 |
Beschreibung Die Funktion consult() im Vertrag ComboOracle wird verwendet, um den Durchschnittspreis aus mehreren Quellen zu berechnen. In der Implementierung von Version 1 wird das arithmetische Mittel zur Berechnung des Endpreises verwendet, was durch Beeinflussung eines der Quellenorakel 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);
}
Listing 3.2: ComboOracle.sol
In der Implementierung von Version 4 wird der niedrigste Preis zurückgegeben, wenn der Durchschnittspreis größer als der niedrigste Preis×1,025 ist. Der Rückgabewert kann jedoch immer noch manipuliert werden, wenn das Ergebnis eines der Quellenorakel ungewöhnlich 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;
}
Listing 3.3: ComboOracle.sol
Auswirkung Der von ComboOracle zurückgegebene Preis kann manipuliert werden, was es dem Angreifer ermöglicht, davon zu profitieren.
Empfehlung Wir empfehlen, den Medianwert anstelle des Durchschnittswerts zu verwenden. Wenn es nur zwei Quellenorakel gibt und ein erheblicher Unterschied auftritt, ist es sinnvoller, die Transaktion rückgängig zu machen, wenn der Durchschnittspreis erheblich größer als der niedrigste Preis ist.
Rückmeldung Es wird nur zwei Quellenorakel geben. Wenn ein erheblicher Unterschied auftritt, werden wir einen OZ Defender Sentinel verwenden, um zugehörige Verträge zu pausieren.
Hinweis Der Vertrag ComboOracle wurde entfernt und wird nicht mehr verwendet.
3.2.2 Potenzielles Problem 3: Potenzieller Mittelabfluss durch BaseBounty
| Element | Beschreibung |
|---|---|
| Schweregrad | Hoch |
| Status | Behoben in Version 4 |
| Eingeführt durch | Version 1 |
Beschreibung Ein Benutzer kann Token (d.h. RDNT) für eine feste Dauer sperren, um Belohnungen zu verdienen. Wenn die Sperre abläuft, können andere Benutzer die Funktion executeBounty() aufrufen, um die Token für diesen Benutzer erneut zu sperren und die BaseBounty zu verdienen, wenn dieser Benutzer AutoRelock aktiviert hat. Während des Neu-Sperrens werden die abgelaufenen Sperren gelöscht und in der internen Funktion _cleanWithdrawableLocks() erneut in den Pool eingesetzt. Es gibt jedoch eine Variable maxLockWithdrawPerTxn, die die maximale Anzahl von Sperren begrenzt, die gelöscht werden können. In diesem Fall können nicht gelöschte abgelaufene Sperren noch vorhanden sein, selbst nachdem die Funktion executeBounty() ausgeführt wurde. Dies kann die Überprüfung in Zeile 106 der Funktion claimBounty() im Vertrag MFDPlus umgehen. Das 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);
}
}
}
Listing 3.4: MultiFeeDistribution.sol
Konkret kann der Angreifer 1 Wei Token mit derselben Ablaufzeit mehrfach einsetzen, was erheblich größer als maxLockWithdrawPerTxn ist. Danach kann der Angreifer die Aktion als getLpMfdBounty festlegen und executeBounty() wiederholt aufrufen. Da die Anzahl der gelöschten Sperren durch die maxLockWithdrawPerTxn begrenzt ist, kann die BaseBounty im Vertrag BountyManager vom Angreifer geleert werden.
Auswirkung Der Angreifer kann alle Mittel im Vertrag BountyManager in einer Transaktion abführen, was zur Störung der vorgesehenen Bounty-Mechanismen führt.
Empfehlung Stellen Sie sicher, dass die Funktion _cleanWithdrawableLocks() alle abgelaufenen Sperren löschen kann, und legen Sie einen Mindest-Einsatzbetrag in der Funktion _stake() fest.
3.2.3 Potenzielles Problem 4: Potentiell ungültige Emissionspläne
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Behoben in Version 10 |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag ChefIncentivesController wird die Funktion setEmissionSchedule() vom Eigentümer aufgerufen, um Zeitpläne für verschiedene Belohnungsraten festzulegen. In diesem Fall sollte die Startzeit für jeden Zeitplan (_startTimeOffsets[i] + startTime) validiert werden, um größer als der aktuelle Zeitstempel zu sein. Es wird jedoch nur das erste Element in _startTimeOffsets überprüft, was nicht ausreichend ist. Darüber hinaus wird _startTimeOffsets[i] von uint256 in uint128 konvertiert, wenn es zu emissionSchedule hinzugefügt wird, 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);
}
Listing 3.5: ChefIncentivesController.sol
Auswirkung Wenn _startTimeOffsets nicht in aufsteigender Reihenfolge 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.
Empfehlung Stellen Sie sicher, dass _startTimeOffsets in aufsteigender Reihenfolge ist und alle Elemente innerhalb des uint128-Bereichs 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 iteriert die Funktion setScheduleRewardsPerSecond() durch emissionSchedule, um den Zielplan mit dem größten Index zu finden, der bereits gestartet wurde, 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);
}
}
}
Listing 3.6: ChefIncentivesController.sol
Auswirkung Wenn die Funktion setScheduledRewardsPerSecond() lange Zeit nicht aufgerufen wird, werden einige versprochene Belohnungen möglicherweise nicht an die Benutzer verteilt.
Empfehlung Die Funktion setScheduledRewardsPerSecond() wird innerhalb der Funktion claim() und _handleActionAfterForToken() aufgerufen, sodass die einzige Möglichkeit, einen Emissionsplan zu überspringen, darin besteht, dass niemand während einer Emissionsepoche mit dem Protokoll interagiert.
3.2.5 Potenzielles Problem 6: Veränderbarer Wechselkurs während der Migration
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | Behoben in Version 5 |
| Eingeführt durch | Version 1 |
Beschreibung Der Vertrag Migration ist für Benutzer implementiert, um von tokenV1 zu tokenV2 mit einem festgelegten exchangeRate zu wechseln. Während des Migrationsprozesses kann dieser exchangeRate jedoch immer noch vom Eigentümer über die Funktion setExchangeRate() 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);
}
Listing 3.7: Migration.sol
Auswirkung Es wäre unfair gegenüber anderen Benutzern, wenn der exchangeRate während des Migrationsprozesses geändert wird.
Empfehlung Sobald die Migration beginnt, sollte der exchangeRate festgelegt sein.
3.2.6 Potenzielles Problem 7: Fehlerhafte Implementierung von _transfer() (I)
| Element | Beschreibung |
|---|---|
| Schweregrad | Hoch |
| Status | Behoben in Version 7 |
| 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 (sogenannte Selbstübertragung). Wenn der Sender gleich dem Empfänger ist, wird das Guthaben des Senders überschrieben, wenn das Guthaben 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);
}
}
}
Listing 3.8: IncentivizedERC20.sol
Auswirkung Token können unendlich geprägt werden.
Empfehlung Implementieren Sie die Funktion _transfer() ordnungsgemäß. Zum Beispiel die Standard-_transfer()-Implementierung von ERC20 in OpenZeppelin.
_balances[sender] = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
_balances[recipient] = _balances[recipient].add(amount);
Listing 3.9: ERC20.sol in OpenZeppelin
3.2.7 Potenzielles Problem 8: Fehlende Überprüfung der Periode in UniV2TwapOracle
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Behoben in Version 9 |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag UniV2TwapOracle wird das Attribut _period in der Funktion 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;
}
Listing 3.10: UniV2TwapOracle.sol
Auswirkung In diesem Fall kann das Orakel unerwartete Werte zurückgeben, wenn _period zu klein ist.
Empfehlung Setzen Sie ein Mindestlimit für _period in den Funktionen initialize und setPeriod.
3.2.8 Potenzielles Problem 9: Nicht rückerstattbare Staub-Token
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | Behoben in Version 5 |
| Eingeführt durch | Version 1 |
Beschreibung Im Vertrag UniswapPoolHelper ist die Funktion zapWETH() dafür ausgelegt, dem Benutzer zu helfen, WETH-Token in LP-Token umzuwandeln. Sie ruft die Funktion addLiquidityWETHOnly() auf, um Liquidität im Pool für LP-Token hinzuzufügen. Bei diesem Prozess können Staub-Token entstehen, die an die Benutzer zurückgegeben werden sollten. Der UniswapPoolHelper implementiert jedoch keine solche Funktionalität zur Handhabung dieser Staub-Token.
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);
}
Listing 3.11: UniswapPoolHelper.sol
Auswirkung Die Staub-Token verbleiben im Vertrag und können von anderen über die Funktion zapTokens(0,0) extrahiert werden.
Empfehlung Implementieren Sie die Funktion zur Rückgabe von Staub-Token nach dem Hinzufügen von Liquidität.
3.2.9 Potenzielles Problem 10: Fehlerhafte Implementierung von _transfer() (II)
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | Behoben in Version 9 |
| 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 entspricht, 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);
}
}
}
Listing 3.12: IncentivizedERC20.sol
Auswirkung Wenn Benutzer an sich selbst übertragen, wird ihr Status im Vertrag ChefIncentivesController nicht ordnungsgemäß aktualisiert, was weitere Probleme bei den Belohnungen verursacht.
Empfehlung Korrigieren Sie den senderBalance in der Funktion handleActionAfter().
3.2.10 Potenzielles Problem 11: Manipulierbare Compound-Belohnungen
| Element | Beschreibung |
|---|---|
| Schweregrad | Mittel |
| Status | Behoben in Version 10 |
| Eingeführt durch | Version 5 |
Beschreibung Im MFDPlus-Vertrag tauscht die Funktion _convertPendingRewardsToWeth() die Belohnungen des Benutzers durch den Uniswap-Router gegen WETH zum Neu-Sperren aus. Es gibt jedoch keine 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
);
Listing 3.13: MFDPlus.sol
Auswirkung Der Angreifer kann die Transaktion per Front-Running manipulieren, um den Preis zu beeinflussen und Gewinn zu erzielen.
Empfehlung 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 | Behoben in Version 9 |
| 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
);
Listing 3.14: LendingPool.sol
Auswirkung Wenn der Leverager zu Beginn nicht festgelegt wurde, könnte ein Angreifer den Leverager auf eine beliebige Adresse setzen und dadurch die Kontrolle über die Logik der Funktion depositWithAutoDLP() erlangen.
Empfehlung Setzen Sie den Leverager in der Funktion initialize() oder fügen Sie eine 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 eigene ETH-Token) oder vestende RDNT- Token in MFD-Verträgen verwenden, um LP-Token (d.h. WETH-RDNT) zu erhalten.
Wenn jedoch Liquidität zum Pool hinzugefügt wird, basiert die Berechnung der benötigten Token auf der Menge der Reserven im Pool, die 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 gegen RDNT-Token im unausgeglichenen Pool zu tauschen, ohne den Slippage zu überprüfen.
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);
}
Listing 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;
}
Listing 3.16: UniswapV2Library.sol
Auswirkung Der Angreifer kann die Transaktion per Front-Running manipulieren, um den Preis zu beeinflussen und Gewinn zu erzielen.
Empfehlung Überprüfen Sie den Slippage in der Funktion addLiquidityWETHOnly() oder stellen Sie sicher, dass sie nur von UniswapPoolHelper aufgerufen werden kann.
3.2.13 Potenzielles Problem 14: Fehlende Überprüfung von borrowRatio in loopETH()
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Behoben in Version 10 |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion loopETH() wird für Hebelkredite verwendet und empfängt einen Parameter borrowRatio, um das Beleihungsverhältnis anzugeben. Der borrowRatio wird jedoch nicht überprüft, bevor die Schleife beginnt.
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);
}
Listing 3.17: Leverager.sol
Auswirkung Der borrowRatio kann höher als RATIO_DIVISOR sein, was dem ursprünglichen Design widerspricht.
Empfehlung Stellen Sie sicher, dass borrowRatio kleiner oder gleich RATIO_DIVISOR ist.
3.2.14 Potenzielles Problem 15: Fehlende Längenüberprüfung zwischen assets und poolIDs in setPoolIDs()
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Behoben in Version 10 |
| 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 überprü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);
}
Listing 3.18: StarBorrow.sol
Auswirkung Die Assets werden nicht den richtigen poolIDs zugewiesen.
Empfehlung Stellen Sie sicher, dass die Längen von assets und poolIDs gleich sind.
3.2.15 Potenzielles Problem 16: Fehlende Widerrufung des mint-Privilegs in addBountyContract()
| Element | Beschreibung |
|---|---|
| Schweregrad | Niedrig |
| Status | Bestätigt |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion addBountyContract() wird verwendet, um den neuen BountyManager festzulegen. Der ursprüngliche Bounty-Vertrag behält jedoch das mint-Privileg, was dem ursprünglichen Design widerspricht.
function addBountyContract(address _bounty) external onlyOwner {
BountyManager = _bounty;
minters[_bounty] = true;
}
Listing 3.19: Leverager.sol
Auswirkung Der veraltete BountyManager hat weiterhin mint-Privilegien.
Empfehlung Widerrufen Sie das mint-Privileg des ursprünglichen BountyManager-Vertrags.
Rückmeldung Die Funktion addBountyContract wird nur einmal aufgerufen, um den BountyManager zu initialisieren.
3.2.16 Potenzielles Problem 17: Minter 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 aufzuzeichnen, die die Berechtigung haben, auf die Funktion 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;
}
Listing 3.20: MultiFeeDistribution.sol
Auswirkung Die veralteten minters können nicht entfernt werden, wenn sie aktualisiert werden.
Empfehlung Implementieren Sie eine privilegierte Funktion zur Änderung der minters.
Rückmeldung Da BountyManager, ChefIncentivesController und MultiFeeDistribution aktualisierbar sind, behalten die minters immer die gleiche Proxy-Adresse.
3.3 Zusätzliche Empfehlung
3.3.1 Potenzielles Problem 18: Gas-Optimierung (zapVestingToLp() in Mfd)
| Element | Beschreibung |
|---|---|
| Status | Behoben in Version 10 |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion zapVestingToLp() kann nur vom Vertrag LockZap aufgerufen werden, um die gesperrten Erträge des Benutzers zu übertragen. Sie iteriert das earnings-Array des Benutzers beginnend ab Index 0 und überprüft, ob die unlockTime größer als der aktuelle Zeitstempel ist. Wenn ja, wird dieser Ertrag aus dem Array entfernt und übertragen. Da die unlockTime im Array mit dem Index zunimmt, wäre es effizienter, die Iteration vom Ende des Arrays zum Anfang zu beginnen. Wenn die unlockTime kleiner als der aktuelle Zeitstempel ist, 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;
}
Listing 3.21: MultiFeeDistribution.sol
Empfehlung Beginnen Sie die Iteration vom Ende der earnings zum Anfang. Wenn die unlockTime kleiner als der aktuelle Zeitstempel ist, kann die Schleife unterbrochen werden.
3.3.2 Potenzielles Problem 19: Nicht leere Bounty-Reserve in BountyManager
| Element | Beschreibung |
|---|---|
| Status | Behoben in Version 10 |
| Eingeführt durch | Version 1 |
Beschreibung In der Funktion _sendBounty() wird das Ereignis BountyReseveEmpty() ausgelöst und der Vertrag pausiert, wenn nicht genügend RDNT-Token für die Übertragung im Vertrag BountyManager vorhanden sind. Es ist jedoch möglich, dass noch einige RDNT-Token übrig sind, was im Widerspruch zum 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;
}
}
Listing 3.22: BountyManager.sol
Empfehlung Übertragen Sie die verbleibenden RDNT-Token, auch wenn sie nicht ausreichen.
3.3.3 Potenzielles Problem 20: Inkonsistente Benennung in requiredUsdValue()
| Element | Beschreibung |
|---|---|
| Status | Bestätigt |
| Eingeführt durch | Version 1 |
Beschreibung Die Funktion requiredUsdValue() wird verwendet, um den erforderlichen gesperrten Wert des Benutzers zu überprüfen, der berechtigt sein möchte, Belohnungen durch das Halten von RTokens zu verdienen. Die Berechnung basiert auf dem Sicherheitenwert des Benutzers, der von der Funktion getUserAccountData() zurückgegeben wird. Der zurückgegebene Wert wird jedoch als totalCollateralETH bezeichnet, was inkonsistent mit der Funktion requiredUsdValue() ist (d.h. totalCollateralUSD).
Empfehlung Standardisieren Sie die Benennungskonventionen von Funktionen mit dem richtigen Token-Namen. Zum Beispiel, benennen Sie requiredUsdValue() in requiredEthValue() um.
Rückmeldung Wir ziehen es vor, die AAVE-Verträge so ähnlich wie möglich zu halten, daher haben wir den Namen nicht aktualisiert.
3.4 Hinweise
3.4.1 Potenzielles Problem 21: Veraltetes 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 verschoben und die andere Logik in den Vertrag MiddleFeeDistribution.
4. Anhang
4.1 Ergebnisse der automatisierten statischen Sicherheitstests
Tabelle 4.1: Ergebnisse der automatisierten statischen Sicherheitstests. Gefunden gibt die Anzahl der von den Tools gemeldeten Probleme an. FP bedeutet 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 beliebigem from | Hoch | 1 | 1 | Bestanden |
| 2 | array-by-reference | Änderung von Speicher-Array 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 | Direkte Änderung von Variablen ohne Zugriffskontrolle | Hoch | 0 | 0 | Bestanden |
| 7 | rtlo | Verwendung des Right-To-Left-Override-Steuerzeichens | Hoch | 0 | 0 | Bestanden |
| 8 | shadowing-state | Shadowing von Zustandsvariablen | Hoch | 1 | 1 | Bestanden |
| 9 | suicidal | Funktionen, die jedem erlauben, den Vertrag zu zerstören | Hoch | 0 | 0 | Bestanden |
| 10 | uninitialized-state | Nicht initialisierte Zustandsvariablen | Hoch | 3 | 3 | Bestanden |
| 11 | uninitialized-storage | Nicht initialisierte Speichervariablen | Hoch | 0 | 0 | Bestanden |
| 12 | unprotected-upgrade | Ungeschützter upgradefähiger Vertrag | Hoch | 1 | 1 | Bestanden |
| 13 | arbitrary-send-erc20-permit | transferFrom verwendet beliebiges from 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 | Manipulierte Array-Längenzuweisung | Hoch | 0 | 0 | Bestanden |
| 16 | controlled-delegatecall | Kontrolliertes delegatecall-Ziel | Hoch | 0 | 0 | Bestanden |
| 17 | delegatecall-loop | Zahlbare Funktionen mit delegatecall innerhalb einer Schleife | Hoch | 0 | 0 | Bestanden |
| 18 | msg-value-loop | Verwendung von msg.value innerhalb einer Schleife | Hoch | 0 | 0 | Bestanden |
| 19 | reentrancy-eth | Reentrancy-Schwachstellen (Diebstahl von Ether) | Hoch | 5 | 5 | Bestanden |
| 20 | storage-array | Compiler-Fehler bei vorzeichenbehaftetem Speicher-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 haben, deren Signatur mit EIP-2612's DOMAIN_SEPARATOR() kollidiert | Mittel | 0 | 0 | Bestanden |
| 24 | enum-conversion | Erkennt gefährliche Enum-Konvertierung | 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öschung einer Mapping-Struktur | Mittel | 0 | 0 | Bestanden |
| 30 | shadowing-abstract | Shadowing von Zustandsvariablen aus abstrakten Verträgen | Mittel | 0 | 0 | Bestanden |
| 31 | tautology | Tautologie oder Widerspruch | Mittel | 0 | 0 | Bestanden |
| 32 | write-after-write | Ungenutzte Schreibvorgänge | Mittel | 3 | 3 | Bestanden |
| 33 | boolean-cst | Missbrauch von booleschen Konstanten | 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 | Unpräzise arithmetische Operationsreihenfolge | Mittel | 20 | 20 | Bestanden |
| 37 | reentrancy-no-eth | Reentrancy-Schwachstellen (kein Diebstahl von Ether) | Mittel | 12 | 12 | Bestanden |
| 38 | reused-constructor | Wiederverwendeter Basiskonstruktor | 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üftes send | Mittel | 0 | 0 | Bestanden |
| 42 | uninitialized-local | Nicht initialisierte 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 kreditbezogene Logik
| ID | Eigenschaft | Ergebnis |
|---|---|---|
| 1 | Der Aufruf von deposit führt niemals zu einer Verringerung des RToken-Betrags von onBehalfOf | Bestanden |
| 2 | Der Aufruf von withdraw führt niemals zu einer Erhöhung des RToken-Betrags von msg.sender | Bestanden |
| 3 | Der Aufruf von borrow mit stabilem Zinsmodus führt niemals zu einer Verringerung des StableDebtToken von onBehalfOf. | Bestanden |
| 4 | Der Aufruf von borrow mit variablem Zinsmodus führt niemals zu einer Verringerung des VariableDebtToken von onBehalfOf. | Bestanden |
| 5 | Der Aufruf von borrow mit onBehalfOf ungleich msg.sender führt niemals zu einer Erhöhung der Krediterlaubnis von msg.sender. | Bestanden |
| 6 | Der Aufruf von repay mit stabilem Zinsmodus führt niemals zu einer Erhöhung des StableDebtToken von onBehalfOf. | Bestanden |
| 7 | Der Aufruf von repay mit variablem Zinsmodus führt niemals zu einer Erhöhung des VariableDebtToken von onBehalfOf. | Bestanden |
| 8 | liquidityIndex wird niemals abnehmen. | Bestanden |
| 9 | liquidityIndex bleibt innerhalb desselben Blocks konstant. | Bestanden |
| 10 | variableBorrowIndex wird niemals abnehmen. | Bestanden |
| 11 | variableBorrowIndex bleibt innerhalb desselben Blocks konstant. | Bestanden |
| 12 | Abnehmende Sicherheitenbeträge führen niemals zu einem Health-Faktor kleiner als 1. | Bestanden |
| 13 | Zunehmende Kreditbeträge führen niemals zu einem Health-Faktor kleiner als 1. | Bestanden |
Tabelle 4.3: Getestete Eigenschaften für stakingbezogene Logik
| ID | Eigenschaft | Ergebnis |
|---|---|---|
| 1 | Das Gesamtguthaben des Benutzers entspricht immer der Summe aus gesperrtem Guthaben, entsperrtem Guthaben und verdientem Guthaben. | Bestanden |
| 2 | Das gesperrte Guthaben des Benutzers entspricht immer der Summe der userLocks-Beträge | Bestanden |
| 3 | Das lockedWithMultiplier-Guthaben des Benutzers entspricht immer der Summe der userLocks-Beträge multipliziert mit dem userLocks-Multiplikator | Bestanden |
| 4 | lockedSupply entspricht immer der Summe der gesperrten Guthaben der Benutzer | Bestanden |
| 5 | lockedSupplyWithMultiplier entspricht immer der Summe der lockedWithMultiplier-Guthaben der Benutzer | Bestanden |
| 6 | rewardPerTokenStored nimmt niemals ab. | Bestanden |
| 7 | rewardPerTokenStored bleibt innerhalb desselben Blocks konstant. | Bestanden |
| 8 | totalSupply entspricht immer der Summe der Benutzerbeträge | Bestanden |
| 9 | accRewardPerShare nimmt niemals ab. | Bestanden |
| 10 | accRewardPerShare bleibt innerhalb desselben Blocks konstant. | Bestanden |
Tabelle 4.4: Getestete Eigenschaften für sonstige Funktionen
| ID | Eigenschaft | Ergebnis |
|---|---|---|
| 1 | WETH- und RDNT-Guthaben des Vertrags LockedZap ist immer null. | Bestanden |
| 2 | WETH- und RDNT-Guthaben des Vertrags LiquidityZap ist immer null. | Bestanden |
| 3 | WETH- und RDNT-Guthaben des Vertrags BalancerPoolHelper ist immer null. | Bestanden |
| 4 | WETH- und RDNT-Guthaben des Vertrags UniswapPoolHelper ist immer null. | Bestanden |
| 5 | Der Aufruf von loop führt immer dazu, dass der Benutzer für Belohnungen berechtigt ist | Bestanden |
| 6 | Der Aufruf von loopETH führt immer dazu, dass der Benutzer für Belohnungen berechtigt ist | Bestanden |
| 7 | Der Aufruf von executeBounty mit _execute gleich false führt niemals zu einer Zustandsänderung im Speicher. | Bestanden |
| 8 | Der Aufruf von transfer mit Sender gleich Empfänger führt niemals zu einer Guthabenänderung. | Fehlgeschlagen in Version 1. Bestanden in Version 7 |
5 Hinweise und Anmerkungen
5.1 Haftungsausschluss
Dieser Bericht stellt keine Anlageberatung oder persönliche Empfehlung dar. Er berücksichtigt nicht und sollte nicht als Berücksichtigung oder als Einfluss auf die potenzielle Wirtschaftlichkeit eines Tokens, Token-Verkaufs oder anderer Produkte, Dienstleistungen oder anderer Vermögenswerte interpretiert werden. Keine Einheit sollte sich in irgendeiner Weise auf diesen Bericht verlassen, auch nicht für den Zweck, Entscheidungen über den Kauf oder Verkauf von Token, Produkten, Dienstleistungen oder anderen Vermögenswerten zu treffen.
Dieser Bericht stellt keine Befürwortung eines bestimmten Projekts oder Teams dar, und der Bericht garantiert nicht die Sicherheit eines bestimmten Projekts. Diese Sicherheitstests geben keine Garantien für die Entdeckung aller Sicherheitsprobleme der Smart Contracts, d.h. das Bewertungsergebnis garantiert nicht das Nichtvorhandensein weiterer Sicherheitsprobleme. Da die Sicherheitstests nicht als umfassend angesehen werden können, empfehlen wir immer, mit unabhängigen Audits und einem öffentlichen Bug-Bounty-Programm fortzufahren, um die Sicherheit von Smart Contracts zu gewährleisten.
Der Umfang dieser Sicherheitstests ist auf den in Abschnitt 1.2 erwähnten Code beschränkt. Sofern nicht ausdrücklich angegeben, sind die Sicherheit der Sprache selbst (z.B. die Solidity-Sprache), die zugrunde liegende Kompilierungs-Toolchain und die Computerinfrastruktur nicht im Umfang enthalten.
5.2 Prüfungsverfahren
Wir führen die Prüfung gemäß dem folgenden Verfahren durch.
-
Schwachstellenerkennung Wir scannen zunächst Smart Contracts mit automatischen Code-Analysatoren und überprüfen dann manuell (verwerfen oder bestätigen) die von ihnen gemeldeten Probleme.
-
Semantische Analyse Wir untersuchen die Geschäftslogik der Smart Contracts und führen weitere Untersuchungen zu möglichen Schwachstellen durch, indem wir ein automatisches Fuzzing-Tool (entwickelt von unserem Forschungsteam) verwenden. Wir analysieren auch manuell mögliche Angriffsszenarien mit unabhängigen Prüfern, um die Ergebnisse gegenzuprüfen.
-
Empfehlung Wir geben nützliche Ratschläge an Entwickler aus der Perspektive guter Programmierpraktiken, einschließlich Gas-Optimierung, Code-Stil usw.
Wir zeigen die wichtigsten konkreten Prüfpunkte im Folgenden.
5.2.1 Software-Sicherheit
-
Reentrancy
-
DoS
-
Zugriffskontrolle
-
Datenverarbeitung und Datenfluss
-
Ausnahmebehandlung
-
Nicht vertrauenswürdige externe Aufrufe und Kontrollfluss
-
Initialisierungskonsistenz
-
Ereignisoperationen
-
Fehleranfällige Zufälligkeit
-
Unsachgemäße Verwendung des Proxy-Systems
5.2.2 DeFi-Sicherheit
-
Semantische Konsistenz
-
Funktionskonsistenz
-
Berechtigungsverwaltung
-
Geschäftslogik
-
Token-Operationen
-
Notfallmechanismus
-
Orakelsicherheit
-
Whitelist und Blacklist
-
Wirtschaftliche Auswirkung
-
Batch-Übertragung
5.2.3 NFT-Sicherheit
-
Doppelte Einträge
-
Überprüfung des Token-Empfängers
-
Off-Chain-Metadatensicherheit
5.2.4 Zusätzliche Empfehlung
-
Gas-Optimierung
-
Code-Qualität und -Stil
Hinweis: Die vorherigen Prüfpunkte sind die wichtigsten. Wir können während des Prüfungsprozesses weitere Prüfpunkte entsprechend der Funktionalität des Projekts verwenden.



