you can see that calling transfer without updating the pool state can lead to an actual transfer amount exceeding the expected 10e18
tokens. The reason why exactly 10e18 is not transferred after the actual updateState is due to a bad implementation of the transfer function, which we'll cover in another report.
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {crvUSDToken} from "src/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "src/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "src/core/tokens/RAACNFT.sol";
import {IRToken, RToken} from "src/core/tokens/RToken.sol";
import {DebtToken} from "src/core/tokens/DebtToken.sol";
import {LendingPool} from "src/core/pools/LendingPool/LendingPool.sol";
import {ReserveLibrary} from "src/libraries/pools/ReserveLibrary.sol";
contract BaseTest is Test {
crvUSDToken public crvUSDTokenInstance;
RAACHousePrices public raacHousePricesInstance;
RAACNFT public raacNFTInstance;
RToken public rTokenInstance;
DebtToken public debtTokenInstance;
LendingPool public lendingPoolInstance;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address hyuunn = makeAddr("hyuunn");
function setUp() public {
crvUSDTokenInstance = new crvUSDToken(address(this));
raacHousePricesInstance = new RAACHousePrices(address(this));
raacHousePricesInstance.setOracle(address(this));
raacNFTInstance = new RAACNFT(
address(crvUSDTokenInstance),
address(raacHousePricesInstance),
address(this)
);
_mintRaacNFT();
rTokenInstance = new RToken(
"RToken",
"RTK",
address(this),
address(crvUSDTokenInstance)
);
debtTokenInstance = new DebtToken("DebtToken", "DEBT", address(this));
lendingPoolInstance = new LendingPool(
address(crvUSDTokenInstance),
address(rTokenInstance),
address(debtTokenInstance),
address(raacNFTInstance),
address(raacHousePricesInstance),
0.1e27
);
rTokenInstance.setReservePool(address(lendingPoolInstance));
debtTokenInstance.setReservePool(address(lendingPoolInstance));
}
function _mintRaacNFT() internal {
raacHousePricesInstance.setHousePrice(0, 100e18);
raacHousePricesInstance.setHousePrice(1, 50e18);
raacHousePricesInstance.setHousePrice(2, 150e18);
deal(address(crvUSDTokenInstance), alice, 1000e18);
deal(address(crvUSDTokenInstance), bob, 1000e18);
deal(address(crvUSDTokenInstance), hyuunn, 1000e18);
vm.startPrank(alice);
crvUSDTokenInstance.approve(address(raacNFTInstance), 100e18 + 1);
raacNFTInstance.mint(0, 100e18 + 1);
vm.stopPrank();
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(raacNFTInstance), 50e18 + 1);
raacNFTInstance.mint(1, 50e18 + 1);
vm.stopPrank();
}
function test_poc_transfer_need_updateState() public {
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(lendingPoolInstance), 500e18);
lendingPoolInstance.deposit(500e18);
raacNFTInstance.approve(address(lendingPoolInstance), 1);
lendingPoolInstance.depositNFT(1);
lendingPoolInstance.borrow(10e18);
vm.stopPrank();
vm.startPrank(alice);
crvUSDTokenInstance.approve(address(lendingPoolInstance), 50e18);
lendingPoolInstance.deposit(50e18);
vm.warp(block.timestamp + 365 days);
uint256 snapshot = vm.snapshotState();
rTokenInstance.transfer(hyuunn, 10e18);
lendingPoolInstance.updateState();
console.log("\\nno updateState before transfer()");
console.log("rTokenInstance.balanceOf(alice): ", rTokenInstance.balanceOf(alice));
console.log("rTokenInstance.balanceOf(hyuunn): ", rTokenInstance.balanceOf(hyuunn));
vm.revertToState(snapshot);
lendingPoolInstance.updateState();
rTokenInstance.transfer(hyuunn, 10e18);
console.log("\\nupdateState before transfer()");
console.log("rTokenInstance.balanceOf(alice): ", rTokenInstance.balanceOf(alice));
console.log("rTokenInstance.balanceOf(hyuunn): ", rTokenInstance.balanceOf(hyuunn));
}
}
Perform updateState before performing any transfer-related logic functions.