diff --git a/abi/AaveIncentives.json b/abi/AaveIncentives.json index dba273e5..8719921c 100644 --- a/abi/AaveIncentives.json +++ b/abi/AaveIncentives.json @@ -29,6 +29,19 @@ "name": "RewardsClaimed", "type": "event" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "donate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "rewardAsset", diff --git a/abi/AavePodCall.json b/abi/AavePodCall.json index 419cef01..5ed949fd 100644 --- a/abi/AavePodCall.json +++ b/abi/AavePodCall.json @@ -356,6 +356,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "donate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/AavePodPut.json b/abi/AavePodPut.json index 419cef01..5ed949fd 100644 --- a/abi/AavePodPut.json +++ b/abi/AavePodPut.json @@ -356,6 +356,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "donate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/OptionAMMPool.json b/abi/OptionAMMPool.json index 1c2e4da7..38797b42 100644 --- a/abi/OptionAMMPool.json +++ b/abi/OptionAMMPool.json @@ -375,6 +375,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "donate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "feePoolA", diff --git a/contracts/options/rewards/AaveIncentives.sol b/contracts/options/rewards/AaveIncentives.sol index 22960dec..adb25d84 100644 --- a/contracts/options/rewards/AaveIncentives.sol +++ b/contracts/options/rewards/AaveIncentives.sol @@ -31,6 +31,18 @@ abstract contract AaveIncentives is Conversion { function _claimRewards(address[] memory assets) internal { IAaveIncentivesController distributor = IAaveIncentivesController(rewardContract); uint256 amountToClaim = distributor.getRewardsBalance(assets, address(this)); - distributor.claimRewards(assets, amountToClaim, address(this)); + uint256 rewardBalance = _rewardBalance(); + + // Don't call "claim" if we have enough to pay option writers + if (rewardBalance < amountToClaim) { + distributor.claimRewards(assets, amountToClaim, address(this)); + } + } + + /** + * @notice Donate rewards to the option + */ + function donate(uint256 amount) external { + require(IERC20(rewardAsset).transferFrom(msg.sender, address(this), amount), "Donation Failed"); } } diff --git a/test/options/rewards/AavePodCall.test.js b/test/options/rewards/AavePodCall.test.js index caac9979..d7d8d5e6 100644 --- a/test/options/rewards/AavePodCall.test.js +++ b/test/options/rewards/AavePodCall.test.js @@ -17,6 +17,14 @@ describe('AavePodCall', () => { const strikePrice = ethers.BigNumber.from(300e18.toString()) const claimable = ethers.BigNumber.from(20e18.toString()) + const setupClaimable = async () => { + await aaveRewardDistributor.mock.getRewardsBalance.returns(claimable) + await aaveRewardDistributor.mock.claimRewards.returns(claimable) + + await rewardToken.connect(deployer).mint(claimable) + await rewardToken.connect(deployer).transfer(option.address, claimable) + } + before(async () => { ;[deployer, minter0, minter1] = await ethers.getSigners() ;[AavePodCall, MintableInterestBearing, configurationManager, aaveRewardDistributor] = await Promise.all([ @@ -39,9 +47,6 @@ describe('AavePodCall', () => { ethers.utils.formatBytes32String('REWARD_CONTRACT'), aaveRewardDistributor.address ) - - await aaveRewardDistributor.mock.getRewardsBalance.returns(claimable) - await aaveRewardDistributor.mock.claimRewards.returns(claimable) }) beforeEach(async () => { @@ -58,9 +63,6 @@ describe('AavePodCall', () => { 24 * 60 * 60, // 24h configurationManager.address ) - - await rewardToken.connect(deployer).mint(claimable) - await rewardToken.connect(deployer).transfer(option.address, claimable) }) afterEach(async () => { @@ -68,6 +70,8 @@ describe('AavePodCall', () => { }) it('unmints entirely and gets the rewards', async () => { + await setupClaimable() + await mintOptions(option, amountToMint, minter0) await option.connect(minter0).unmint(amountToMint) expect(await option.balanceOf(minter0.address)).to.be.equal(0) @@ -76,6 +80,8 @@ describe('AavePodCall', () => { }) it('unmints partially and gets partial rewards', async () => { + await setupClaimable() + await mintOptions(option, amountToMint, minter0) await mintOptions(option, amountToMint, minter1) @@ -96,8 +102,32 @@ describe('AavePodCall', () => { }) it('withdraws and gets the rewards', async () => { + await setupClaimable() + + await mintOptions(option, amountToMint, minter0) + await skipToWithdrawWindow(option) + await option.connect(minter0).withdraw() + expect(await option.shares(minter0.address)).to.be.equal(0) + expect(await underlyingAsset.balanceOf(minter0.address)).to.be.equal(amountToMint) + expect(await rewardToken.balanceOf(minter0.address)).to.be.equal(claimable) + }) + + it('accepts donations', async () => { + // Setup Distributor to fail when claiming rewards + await aaveRewardDistributor.mock.getRewardsBalance.returns(claimable) + await aaveRewardDistributor.mock.claimRewards.reverts() + await mintOptions(option, amountToMint, minter0) await skipToWithdrawWindow(option) + // Withdrawing with failing distributor + const tx = option.connect(minter0).withdraw() + await expect(tx).to.be.reverted + + // Subsidizing rewards + await rewardToken.connect(deployer).mint(claimable) + await rewardToken.connect(deployer).approve(option.address, claimable) + await option.connect(deployer).donate(claimable) + await option.connect(minter0).withdraw() expect(await option.shares(minter0.address)).to.be.equal(0) expect(await underlyingAsset.balanceOf(minter0.address)).to.be.equal(amountToMint) diff --git a/test/options/rewards/AavePodPut.test.js b/test/options/rewards/AavePodPut.test.js index 3d817e39..5764abcb 100644 --- a/test/options/rewards/AavePodPut.test.js +++ b/test/options/rewards/AavePodPut.test.js @@ -17,6 +17,14 @@ describe('AavePodPut', () => { const strikePrice = ethers.BigNumber.from(300e18.toString()) const claimable = ethers.BigNumber.from(20e18.toString()) + const setupClaimable = async () => { + await aaveRewardDistributor.mock.getRewardsBalance.returns(claimable) + await aaveRewardDistributor.mock.claimRewards.returns(claimable) + + await rewardToken.connect(deployer).mint(claimable) + await rewardToken.connect(deployer).transfer(option.address, claimable) + } + before(async () => { ;[deployer, minter0, minter1] = await ethers.getSigners() ;[AavePodPut, MintableInterestBearing, configurationManager, aaveRewardDistributor] = await Promise.all([ @@ -39,9 +47,6 @@ describe('AavePodPut', () => { ethers.utils.formatBytes32String('REWARD_CONTRACT'), aaveRewardDistributor.address ) - - await aaveRewardDistributor.mock.getRewardsBalance.returns(claimable) - await aaveRewardDistributor.mock.claimRewards.returns(claimable) }) beforeEach(async () => { @@ -58,9 +63,6 @@ describe('AavePodPut', () => { 24 * 60 * 60, // 24h configurationManager.address ) - - await rewardToken.connect(deployer).mint(claimable) - await rewardToken.connect(deployer).transfer(option.address, claimable) }) afterEach(async () => { @@ -68,6 +70,8 @@ describe('AavePodPut', () => { }) it('unmints entirely and gets the rewards', async () => { + await setupClaimable() + await mintOptions(option, amountToMint, minter0) await option.connect(minter0).unmint(amountToMint) expect(await option.balanceOf(minter0.address)).to.be.equal(0) @@ -76,6 +80,7 @@ describe('AavePodPut', () => { }) it('unmints partially and gets partial rewards', async () => { + await setupClaimable() await mintOptions(option, amountToMint, minter0) await mintOptions(option, amountToMint, minter1) @@ -96,6 +101,7 @@ describe('AavePodPut', () => { }) it('withdraws and gets the rewards', async () => { + await setupClaimable() await mintOptions(option, amountToMint, minter0) await skipToWithdrawWindow(option) await option.connect(minter0).withdraw() @@ -103,4 +109,26 @@ describe('AavePodPut', () => { expect(await strikeAsset.balanceOf(minter0.address)).to.be.equal(strikePrice) expect(await rewardToken.balanceOf(minter0.address)).to.be.equal(claimable) }) + + it('accepts donations', async () => { + // Setup Distributor to fail when claiming rewards + await aaveRewardDistributor.mock.getRewardsBalance.returns(claimable) + await aaveRewardDistributor.mock.claimRewards.reverts() + + await mintOptions(option, amountToMint, minter0) + await skipToWithdrawWindow(option) + // Withdrawing with failing distributor + const tx = option.connect(minter0).withdraw() + await expect(tx).to.be.reverted + + // Subsidizing rewards + await rewardToken.connect(deployer).mint(claimable) + await rewardToken.connect(deployer).approve(option.address, claimable) + await option.connect(deployer).donate(claimable) + + await option.connect(minter0).withdraw() + expect(await option.shares(minter0.address)).to.be.equal(0) + expect(await strikeAsset.balanceOf(minter0.address)).to.be.equal(strikePrice) + expect(await rewardToken.balanceOf(minter0.address)).to.be.equal(claimable) + }) })