Interesting Brick Yeti
Medium
The Blacklist::addBlackList()
function doesn't check if the provided user is the Stablecoin contract which will cause to bypass SUPPORT_ROLE and withdraw big portion of the accidentally sent tokens to the Stablecoin contract.
If the BLACKLISTER_ROLE
provides the Stablecoin's contract address as a function parameter accidentally (it is possible and a valid case according to the project's README) or if it is malicious, the Stablecoin native tokens which are accidentally sent to the Stablecoin contract will be withdrawn to the BLACKLISTER_ROLE
address.
Blacklist.sol
function addBlackList(
address user
) public virtual onlyRole(BLACKLISTER_ROLE) {
if (blacklisted(user)) revert AlreadyBlacklisted(user);
_onceBlacklisted(user);
// do this after so it doesnt trip blacklisted restirctions
_setBlacklist(user, true);
emit AddedBlacklist(user);
}
Stablecoin.sol
function _onceBlacklisted(address user) internal override {
_transfer(user, _msgSender(), balanceOf(user));
}
If the SUPPORT_ROLE
tries to rescue the funds, the Stablecoin::erc20Rescue()
function will revert, because the address is blacklisted. Even the BLACKLISTER_ROLE
calls removeBlackList()
to unblacklist the user, the tokens will be already sent.
The BLACKLISTER_ROLE
uses the functionality which is meant to be used from SUPPORT_ROLE
.
A user/s needs to accidentally sent Stablecoin tokens to the Stablecoin contract. It is very likely to happen and there are many real world examples. Big percentage of the accidentally sent tokens are the contract's own tokens. Some examples - OMG, WETH (Example1, Example2), etc.
No response
- A user accidentally sends 100 Stablecoin tokens to the Stablecoin contract
- The MINTER_ROLE calls addBlackList() function with Stablecoin's address as a parameter which transfers all the Stablecoin tokens of the Stablecoin contract to his address
- SUPPORT_ROLE sees there are tokens sent to the contract and tries to transfer them back to the user
- The erc20Rescue() function reverts, because the Stablecoin contract's address is blacklisted
The BLACKLISTER_ROLE uses SUPPORT_ROLE's functionality and it is possible the user, who accidentally sent tokens to the Stablecoin contract, doesn't get his tokens if the BLACKLISTER_ROLE is a smart contract and doesn't have a functionality to send back tokens.
Add the following test case in Stablecoin.test.ts
describe.only("Call addBlackList with Stablecoin's address", () => {
it("blacklist", async () => {
const users = await ethers.getSigners();
let randomUser = users[2];
let blacklister = users[3];
let support = users[3];
await stablecoin.grantRole(MINTER_ROLE, deployer);
await stablecoin.grantRole(BLACKLISTER_ROLE, blacklister);
await stablecoin.grantRole(SUPPORT_ROLE, support);
// A random user has 100 Stablecoin tokens
await stablecoin.mintTo(randomUser, 100);
// The user accidentally sends the tokens to the contract
await stablecoin.connect(randomUser).transfer(stablecoin, 100);
// The minter calls addBlackList with Stablecoin's address as a user which transfers all the Stablecoin tokens to his address
await stablecoin.connect(blacklister).addBlackList(stablecoin);
// Support sees the tokens are sent to the contract and tries to transfer them back to the user
await expect(stablecoin.connect(support).erc20Rescue(stablecoin, randomUser, 100)).to.be.reverted;
expect(await stablecoin.balanceOf(blacklister)).to.equal(100);
expect(await stablecoin.balanceOf(randomUser)).to.equal(0);
expect(await stablecoin.balanceOf(stablecoin)).to.equal(0);
});
});
Check if the user
in _onceBlacklisted
is the Stablecoin contract.
function _onceBlacklisted(address user) internal override {
+ require(user != address(this), "user can't be Stablecoin contract");
_transfer(user, _msgSender(), balanceOf(user));
}