Skip to content

Commit 7c95843

Browse files
authored
Merge pull request #664 from VenusProtocol/feat/VPD-808
[VPD-808] Add internal cash to prevent donation attack
2 parents 9c6467d + e1b9231 commit 7c95843

17 files changed

+1618
-75
lines changed
1.8 MB
Binary file not shown.
2.08 MB
Binary file not shown.

contracts/Tokens/VTokens/VBep20.sol

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ pragma solidity 0.8.25;
44

55
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
66
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7-
87
import { ComptrollerInterface } from "../../Comptroller/ComptrollerInterface.sol";
98
import { InterestRateModelV8 } from "../../InterestRateModels/InterestRateModelV8.sol";
109
import { VBep20Interface, VTokenInterface } from "./VTokenInterfaces.sol";
@@ -214,6 +213,7 @@ contract VBep20 is VToken, VBep20Interface {
214213
* @dev Similar to ERC-20 transfer, but handles tokens that have transfer fees.
215214
* This function returns the actual amount received,
216215
* which may be less than `amount` if there is a fee attached to the transfer.
216+
* Increments `internalCash` by the actual amount received.
217217
* @param from Sender of the underlying tokens
218218
* @param amount Amount of underlying to transfer
219219
* @return Actual amount received
@@ -223,26 +223,49 @@ contract VBep20 is VToken, VBep20Interface {
223223
uint256 balanceBefore = token.balanceOf(address(this));
224224
token.safeTransferFrom(from, address(this), amount);
225225
uint256 balanceAfter = token.balanceOf(address(this));
226+
uint256 actualAmount = balanceAfter - balanceBefore;
227+
internalCash += actualAmount;
226228
// Return the amount that was *actually* transferred
227-
return balanceAfter - balanceBefore;
229+
return actualAmount;
228230
}
229231

230232
/**
231-
* @dev Just a regular ERC-20 transfer, reverts on failure
233+
* @dev Just a regular ERC-20 transfer, reverts on failure.
234+
* Decrements `internalCash` by `amount` before transferring.
232235
* @param to Receiver of the underlying tokens
233236
* @param amount Amount of underlying to transfer
234237
*/
235238
function doTransferOut(address payable to, uint256 amount) internal virtual override {
239+
internalCash -= amount;
236240
IERC20 token = IERC20(underlying);
237241
token.safeTransfer(to, amount);
238242
}
239243

240244
/**
241-
* @notice Gets balance of this contract in terms of the underlying
242-
* @dev This excludes the value of the current message, if any
243-
* @return The quantity of underlying tokens owned by this contract
245+
* @notice Gets the tracked internal cash balance of this contract
246+
* @dev Returns `internalCash` rather than the actual token balance, making it immune to donation attacks.
247+
* @return The internally tracked cash balance of underlying tokens
244248
*/
245249
function getCashPrior() internal view override returns (uint) {
246-
return IERC20(underlying).balanceOf(address(this));
250+
return internalCash;
251+
}
252+
253+
/**
254+
* @notice Transfer excess tokens to caller and sync internalCash with actual balance
255+
* @dev Admin-only. For migration: pass 0 (just syncs). For sweep: pass the excess amount.
256+
* Transfers `transferAmount` of underlying to msg.sender, then sets internalCash = balanceOf(address(this)).
257+
* @param transferAmount Amount of underlying to transfer to msg.sender before syncing
258+
*/
259+
function sweepTokenAndSync(uint256 transferAmount) external {
260+
require(msg.sender == admin);
261+
262+
if (transferAmount > 0) {
263+
IERC20(underlying).safeTransfer(msg.sender, transferAmount);
264+
emit TokenSwept(msg.sender, transferAmount);
265+
}
266+
267+
uint256 oldInternalCash = internalCash;
268+
internalCash = IERC20(underlying).balanceOf(address(this));
269+
emit CashSynced(oldInternalCash, internalCash);
247270
}
248271
}

contracts/Tokens/VTokens/VTokenInterfaces.sol

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,18 @@ contract VTokenStorage is VTokenStorageBase {
172172
*/
173173
uint256 public flashLoanAmount;
174174

175+
/**
176+
* @notice Tracked internal cash balance, immune to direct token transfers (donation attacks)
177+
* @dev Updated only via doTransferIn/doTransferOut. Must be initialized via sweepTokenAndSync() after upgrade.
178+
*/
179+
uint256 public internalCash;
180+
175181
/**
176182
* @dev This empty reserved space is put in place to allow future versions to add new
177183
* variables without shifting down storage in the inheritance chain.
178184
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
179185
*/
180-
uint256[46] private __gap;
186+
uint256[45] private __gap;
181187
}
182188

183189
abstract contract VTokenInterface is VTokenStorage {
@@ -320,6 +326,16 @@ abstract contract VTokenInterface is VTokenStorage {
320326
uint256 protocolFee
321327
);
322328

329+
/**
330+
* @notice Event emitted when internalCash is synced with actual token balance
331+
*/
332+
event CashSynced(uint256 oldInternalCash, uint256 newInternalCash);
333+
334+
/**
335+
* @notice Event emitted when excess tokens are swept by admin
336+
*/
337+
event TokenSwept(address indexed recipient, uint256 amount);
338+
323339
/**
324340
* @notice Event emitted when flashLoan fee mantissa is updated
325341
*/

contracts/test/EvilXToken.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,8 @@ contract EvilXToken is VBep20Delegate {
206206
function harnessCallBorrowAllowed(uint amount) public returns (uint) {
207207
return comptroller.borrowAllowed(address(this), msg.sender, amount);
208208
}
209+
210+
function harnessSetInternalCash(uint256 _internalCash) public {
211+
internalCash = _internalCash;
212+
}
209213
}

contracts/test/VBep20Harness.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ contract VBep20Harness is VBep20Immutable {
166166
function harnessCallBorrowAllowed(uint amount) public returns (uint) {
167167
return comptroller.borrowAllowed(address(this), msg.sender, amount);
168168
}
169+
170+
function harnessSetInternalCash(uint256 _internalCash) public {
171+
internalCash = _internalCash;
172+
}
169173
}
170174

171175
contract VBep20Scenario is VBep20Immutable {
@@ -415,6 +419,10 @@ contract VBep20DelegateHarness is VBep20Delegate {
415419
function harnessCallBorrowAllowed(uint amount) public returns (uint) {
416420
return comptroller.borrowAllowed(address(this), msg.sender, amount);
417421
}
422+
423+
function harnessSetInternalCash(uint256 _internalCash) public {
424+
internalCash = _internalCash;
425+
}
418426
}
419427

420428
contract VBep20DelegateScenario is VBep20Delegate {

contracts/test/VBep20MockDelegate.sol

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
44
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
55

66
import { ComptrollerInterface } from "../Comptroller/ComptrollerInterface.sol";
7-
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
87
import { VToken } from "../Tokens/VTokens/VToken.sol";
98
import { InterestRateModelV8 } from "../InterestRateModels/InterestRateModelV8.sol";
109
import { VBep20Interface, VTokenInterface } from "../Tokens/VTokens/VTokenInterfaces.sol";
@@ -182,20 +181,20 @@ contract VBep20MockDelegate is VToken, VBep20Interface {
182181
/*** Safe Token ***/
183182

184183
/**
185-
* @notice Gets balance of this contract in terms of the underlying
186-
* @dev This excludes the value of the current message, if any
187-
* @return The quantity of underlying tokens owned by this contract
184+
* @notice Gets the tracked internal cash balance of this contract
185+
* @dev Returns `internalCash` rather than the actual token balance, making it immune to donation attacks.
186+
* @return The internally tracked cash balance of underlying tokens
188187
*/
189188
function getCashPrior() internal view override returns (uint) {
190-
IERC20 token = IERC20(underlying);
191-
return token.balanceOf(address(this));
189+
return internalCash;
192190
}
193191

194192
/**
195193
* @dev Similar to EIP20 transfer, except it handles a False result from `transferFrom` and reverts in that case.
196194
* This will revert due to insufficient balance or insufficient allowance.
197195
* This function returns the actual amount received,
198196
* which may be less than `amount` if there is a fee attached to the transfer.
197+
* Increments `internalCash` by the actual amount received.
199198
*
200199
* Note: This wrapper safely handles non-standard BEP-20 tokens that do not return a value.
201200
* See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
@@ -206,20 +205,28 @@ contract VBep20MockDelegate is VToken, VBep20Interface {
206205
token.safeTransferFrom(from, address(this), amount);
207206
// Calculate the amount that was *actually* transferred
208207
uint balanceAfter = IERC20(underlying).balanceOf(address(this));
209-
return balanceAfter - balanceBefore;
208+
uint actualAmount = balanceAfter - balanceBefore;
209+
internalCash += actualAmount;
210+
return actualAmount;
210211
}
211212

212213
/**
213214
* @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory
214215
* error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to
215216
* insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified
216217
* it is >= amount, this should not revert in normal conditions.
218+
* Decrements `internalCash` by `amount` before transferring.
217219
*
218220
* Note: This wrapper safely handles non-standard BEP-20 tokens that do not return a value.
219221
* See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
220222
*/
221223
function doTransferOut(address payable to, uint amount) internal override {
224+
internalCash -= amount;
222225
IERC20 token = IERC20(underlying);
223226
token.safeTransfer(to, amount);
224227
}
228+
229+
function harnessSetInternalCash(uint256 _internalCash) public {
230+
internalCash = _internalCash;
231+
}
225232
}

deployments/bscmainnet.json

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70440,7 +70440,7 @@
7044070440
]
7044170441
},
7044270442
"VBep20Delegate": {
70443-
"address": "0x1be1CE8352328278Ac4e0488436c0f1607282550",
70443+
"address": "0xb25b57599BA969c4829699F7E4Fc4076D14745E1",
7044470444
"abi": [
7044570445
{
7044670446
"inputs": [],
@@ -70597,6 +70597,25 @@
7059770597
"name": "Borrow",
7059870598
"type": "event"
7059970599
},
70600+
{
70601+
"anonymous": false,
70602+
"inputs": [
70603+
{
70604+
"indexed": false,
70605+
"internalType": "uint256",
70606+
"name": "oldInternalCash",
70607+
"type": "uint256"
70608+
},
70609+
{
70610+
"indexed": false,
70611+
"internalType": "uint256",
70612+
"name": "newInternalCash",
70613+
"type": "uint256"
70614+
}
70615+
],
70616+
"name": "CashSynced",
70617+
"type": "event"
70618+
},
7060070619
{
7060170620
"anonymous": false,
7060270621
"inputs": [
@@ -71072,6 +71091,25 @@
7107271091
"name": "ReservesReduced",
7107371092
"type": "event"
7107471093
},
71094+
{
71095+
"anonymous": false,
71096+
"inputs": [
71097+
{
71098+
"indexed": true,
71099+
"internalType": "address",
71100+
"name": "recipient",
71101+
"type": "address"
71102+
},
71103+
{
71104+
"indexed": false,
71105+
"internalType": "uint256",
71106+
"name": "amount",
71107+
"type": "uint256"
71108+
}
71109+
],
71110+
"name": "TokenSwept",
71111+
"type": "event"
71112+
},
7107571113
{
7107671114
"anonymous": false,
7107771115
"inputs": [
@@ -71844,6 +71882,19 @@
7184471882
"stateMutability": "view",
7184571883
"type": "function"
7184671884
},
71885+
{
71886+
"inputs": [],
71887+
"name": "internalCash",
71888+
"outputs": [
71889+
{
71890+
"internalType": "uint256",
71891+
"name": "",
71892+
"type": "uint256"
71893+
}
71894+
],
71895+
"stateMutability": "view",
71896+
"type": "function"
71897+
},
7184771898
{
7184871899
"inputs": [],
7184971900
"name": "isFlashLoanEnabled",
@@ -72279,6 +72330,19 @@
7227972330
"stateMutability": "view",
7228072331
"type": "function"
7228172332
},
72333+
{
72334+
"inputs": [
72335+
{
72336+
"internalType": "uint256",
72337+
"name": "transferAmount",
72338+
"type": "uint256"
72339+
}
72340+
],
72341+
"name": "sweepTokenAndSync",
72342+
"outputs": [],
72343+
"stateMutability": "nonpayable",
72344+
"type": "function"
72345+
},
7228272346
{
7228372347
"inputs": [],
7228472348
"name": "symbol",

0 commit comments

Comments
 (0)