Number of coupon tokens obtained from the auction may differ from number of coupon tokens to be distributed
Description
The pool creates auctions to acquire coupon tokens for distribution. The number of coupon tokens the pool will acquire is based on the current total supply of bond tokens and the _sharesPerToken
value obtained from the globalPool
variable of the bondToken
.
function startAuction() external {
// [...]
// Check if auction for current period has already started
(uint256 currentPeriod, uint256 _sharesPerToken) = bondToken.globalPool();
require(auctions[currentPeriod] == address(0), AuctionAlreadyStarted());
auctions[currentPeriod] = Utils.deploy(
address(new Auction()),
abi.encodeWithSelector(
Auction.initialize.selector,
address(couponToken),
address(reserveToken),
! (bondToken.totalSupply() * _sharesPerToken).toBaseUnit(bondToken.SHARES_DECIMALS()),
block.timestamp + auctionPeriod,
1000,
address(this),
liquidationThreshold
)
);
}
If the auction succeeds, the pool will transfer the obtained coupon tokens to the distributor. The number of coupon tokens transferred is based on the current total supply of bond tokens and the state variable sharesPerToken
. But these two values may differ from when the startAuction
function is called.
function distribute() external whenNotPaused auctionSucceeded {
// [...]
Distributor distributor = Distributor(poolFactory.distributor());
// [...]
uint256 normalizedTotalSupply = bondToken.totalSupply().normalizeAmount(bondDecimals, maxDecimals);
uint256 normalizedShares = sharesPerToken.normalizeAmount(sharesDecimals, maxDecimals);
// Calculate the coupon amount to distribute
uint256 couponAmountToDistribute = (normalizedTotalSupply * normalizedShares)
.toBaseUnit(maxDecimals * 2 - IERC20(couponToken).safeDecimals());
// Increase the bond token period
bondToken.increaseIndexedAssetPeriod(sharesPerToken);
// Transfer coupon tokens to the distributor
IERC20(couponToken).safeTransfer(address(distributor), couponAmountToDistribute);
// [...]
}
The total supply of bond tokens increases or decreases as users deposit or redeem. Additionally, during distribution, users holding more bond tokens can claim more coupon tokens.
function _create(
// [...]
) private returns(uint256) {
// [...]
// Mint tokens
if (tokenType == TokenType.BOND) {
bondToken.mint(recipient, amount);
}
// [...]
}
function _redeem(
// [...]
) private returns(uint256) {
// [...]
// Burn derivative tokens
if (tokenType == TokenType.BOND) {
bondToken.burn(msg.sender, depositAmount);
}
// [...]
}
When not in auction, addresses with GOV_ROLE
can modify the state variable sharesPerToken
. The shares-per-token value stored in the state variable globalPool
of the bondToken
can only be updated to the value set in the pool during each distribution (i.e., after a successful auction).
function setSharesPerToken(uint256 _sharesPerToken) external NotInAuction onlyRole(poolFactory.GOV_ROLE()) {
sharesPerToken = _sharesPerToken;
emit SharesPerTokenChanged(sharesPerToken);
}
Impact
Users may deposit before the distribution to acquire more bond tokens in order to be able to claim more coupon tokens.
For the pool, an inconsistent coupon amount may result in the pool not having enough coupon tokens to transfer to the distributor or leaving some coupon tokens remaining in the pool.
Recommendations
Consider prohibiting users from depositing or redeeming during the auction period.
For shares per token, consider using the value obtained from the globalPool
variable of bondToken
in both the startAuction
and distribute
functions.
Remediation
This issue has been acknowledged by Plaza Finance, and fixes were implemented in the following commits: