diff --git a/contracts/interfaces/IMToken.sol b/contracts/interfaces/IMToken.sol index ae7868b2..381a151f 100644 --- a/contracts/interfaces/IMToken.sol +++ b/contracts/interfaces/IMToken.sol @@ -17,13 +17,22 @@ interface IMToken is IERC20Upgradeable { function mint(address to, uint256 amount) external; /** - * @notice burns mToken token `amount` to a given `to` address. + * @notice burns mToken token `amount` from a given `from` address. * should be called only from permissioned actor * @param from addres to burn tokens from * @param amount amount to burn */ function burn(address from, uint256 amount) external; + /** + * @notice burns mToken token `amount` from a given `from` address, + * bypassing blacklist checks. + * should be called only from permissioned actor + * @param from address to burn tokens from + * @param amount amount to burn + */ + function forceBurn(address from, uint256 amount) external; + /** * @notice updates contract`s metadata. * should be called only from permissioned actor diff --git a/contracts/mToken.sol b/contracts/mToken.sol index 05ed0545..83047c1a 100644 --- a/contracts/mToken.sol +++ b/contracts/mToken.sol @@ -48,6 +48,17 @@ abstract contract mToken is ERC20PausableUpgradeable, Blacklistable, IMToken { function burn(address from, uint256 amount) external onlyRole(_burnerRole(), msg.sender) + { + _onlyNotBlacklisted(from); + _burn(from, amount); + } + + /** + * @inheritdoc IMToken + */ + function forceBurn(address from, uint256 amount) + external + onlyRole(_burnerRole(), msg.sender) { _burn(from, amount); } diff --git a/test/common/token.tests.ts b/test/common/token.tests.ts index 1fe3f2d7..40f20247 100644 --- a/test/common/token.tests.ts +++ b/test/common/token.tests.ts @@ -609,7 +609,7 @@ export const tokenContractsTests = (token: MTokenName) => { ).revertedWith(acErrors.WMAC_HAS_ROLE); }); - it('burn(...) when address is blacklisted', async () => { + it('should fail: burn(...) when address is blacklisted', async () => { const { owner, regularAccounts, accessControl, tokenContract } = await deployMTokenWithFixture(); @@ -620,7 +620,45 @@ export const tokenContractsTests = (token: MTokenName) => { { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); - await burn({ tokenContract, owner }, blacklisted, 1); + await burn({ tokenContract, owner }, blacklisted, 1, { + revertMessage: acErrors.WMAC_HAS_ROLE, + }); + }); + + it('forceBurn(...) when address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await deployMTokenWithFixture(); + + const blacklisted = regularAccounts[0]; + + await mint({ tokenContract, owner }, blacklisted, 1); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + + const balanceBefore = await tokenContract.balanceOf( + blacklisted.address, + ); + await expect( + tokenContract.connect(owner).forceBurn(blacklisted.address, 1), + ).to.not.reverted; + const balanceAfter = await tokenContract.balanceOf(blacklisted.address); + expect(balanceBefore.sub(balanceAfter)).eq(1); + }); + + it('should fail: forceBurn(...) when caller lacks burner role', async () => { + const { owner, regularAccounts, tokenContract } = + await deployMTokenWithFixture(); + + const unauthorized = regularAccounts[0]; + const target = regularAccounts[1]; + + await mint({ tokenContract, owner }, target, 1); + + await expect( + tokenContract.connect(unauthorized).forceBurn(target.address, 1), + ).revertedWith(acErrors.WMAC_HASNT_ROLE); }); it('transferFrom(...) when caller address is blacklisted', async () => { diff --git a/test/unit/LayerZero.test.ts b/test/unit/LayerZero.test.ts index a2106b91..46f9ae7d 100644 --- a/test/unit/LayerZero.test.ts +++ b/test/unit/LayerZero.test.ts @@ -12,6 +12,7 @@ import { MidasLzOFTAdapter__factory, MidasLzVaultComposerSyncTester, } from '../../typechain-types'; +import { acErrors, blackList } from '../common/ac.helpers'; import { approveBase18, mintToken } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; import { deployProxyContract } from '../common/deploy.helpers'; @@ -149,6 +150,33 @@ describe('LayerZero', function () { await sendOft(fixture, { amount: 100 }, { revertOnDst: true }); }); + it('should fail: from A to B when sender is blacklisted', async () => { + const fixture = await loadFixture(layerZeroFixture); + const { owner, regularAccounts, accessControl, mTBILL } = fixture; + + const blacklisted = regularAccounts[0]; + + await mint( + { owner, tokenContract: mTBILL }, + blacklisted, + parseUnits('100', 18), + ); + + await blackList( + { blacklistable: mTBILL, accessControl, owner }, + blacklisted, + ); + + await sendOft( + fixture, + { amount: 100 }, + { + from: blacklisted, + revertMessage: acErrors.WMAC_HAS_ROLE, + }, + ); + }); + it('should fail: send mTBILL from A to B with rate limit exceeded', async () => { const fixture = await loadFixture(layerZeroFixture); const { oftAdapterA, eidB } = fixture;