Summary
The DEToken contract lacks functionality to recover accidentally sent tokens. When ERC20 tokens other than RToken are mistakenly sent to the contract, they become permanently locked.
Vulnerability Details
The contract only handles RToken transfers through transferAsset() but has no mechanism to recover other ERC20 tokens. This is particularly problematic since the contract is meant to interact with multiple tokens in the RAAC ecosystem.
POC (Proof of Concept)
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("DEToken Token Recovery Vulnerability", function () {
let DEToken, deToken;
let MockERC20, mockToken;
let owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
MockERC20 = await ethers.getContractFactory("MockERC20");
mockToken = await MockERC20.deploy("Mock", "MCK");
DEToken = await ethers.getContractFactory("DEToken");
deToken = await DEToken.deploy(
"Debitum Emptor",
"DE",
owner.address,
mockToken.address
);
await mockToken.mint(user.address, ethers.utils.parseEther("100"));
});
it("should demonstrate tokens can be locked in contract", async function () {
await mockToken.connect(user).transfer(
deToken.address,
ethers.utils.parseEther("10")
);
expect(await mockToken.balanceOf(deToken.address))
.to.equal(ethers.utils.parseEther("10"));
await expect(
deToken.connect(owner).transfer(
owner.address,
ethers.utils.parseEther("10")
)
).to.be.revertedWith("OnlyStabilityPool");
});
});
Impact
Severity: Low
Permanent loss of accidentally transferred tokens
No administrative recovery mechanism
Could affect protocol's ability to handle emergencies
Risk increases with protocol adoption and multi-token interactions
Tools Used
Recommendations
Add token recovery functionality:
contract DEToken is ERC20, ERC20Permit, IDEToken, Ownable {
error CannotRescueRToken();
event TokenRescued(address token, address to, uint256 amount);
* @notice Recovers ERC20 tokens accidentally sent to contract
* @param token Address of token to recover
* @param to Address to send recovered tokens to
* @param amount Amount of tokens to recover
*/
function rescueToken(
address token,
address to,
uint256 amount
) external onlyOwner {
if (token == rTokenAddress) revert CannotRescueRToken();
if (to == address(0)) revert InvalidAddress();
if (amount == 0) revert InvalidAmount();
IERC20(token).safeTransfer(to, amount);
emit TokenRescued(token, to, amount);
}
}
Add rescue function test:
it("should allow owner to rescue accidentally sent tokens", async function () {
const OtherToken = await ethers.getContractFactory("MockERC20");
const otherToken = await OtherToken.deploy("Other", "OTH");
await otherToken.mint(user.address, ethers.utils.parseEther("100"));
await otherToken.connect(user).transfer(
deToken.address,
ethers.utils.parseEther("10")
);
await expect(
deToken.connect(owner).rescueToken(
otherToken.address,
owner.address,
ethers.utils.parseEther("10")
)
)
.to.emit(deToken, "TokenRescued")
.withArgs(
otherToken.address,
owner.address,
ethers.utils.parseEther("10")
);
expect(await otherToken.balanceOf(owner.address))
.to.equal(ethers.utils.parseEther("10"));
});