Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redemption fee #92

Merged
merged 10 commits into from
Jul 14, 2020
38 changes: 28 additions & 10 deletions contracts/masset/Masset.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
* @notice The Masset is a token that allows minting and redemption at a 1:1 ratio
* for underlying basket assets (bAssets) of the same peg (i.e. USD,
* EUR, Gold). Composition and validation is enforced via the BasketManager.
* @dev VERSION: 1.0
* DATE: 2020-05-05
* @dev VERSION: 1.1
* DATE: 2020-06-30
*/
contract Masset is
Initializable,
Expand All @@ -48,6 +48,7 @@ contract Masset is

// State Events
event SwapFeeChanged(uint256 fee);
event RedemptionFeeChanged(uint256 fee);
event ForgeValidatorChanged(address forgeValidator);

// Modules and connectors
Expand All @@ -59,6 +60,9 @@ contract Masset is
uint256 public swapFee;
uint256 private MAX_FEE;

// RELEASE 1.1 VARS
uint256 public redemptionFee;

/**
* @dev Constructor
* @notice To avoid variable shadowing appended `Arg` after arguments name.
Expand Down Expand Up @@ -535,8 +539,11 @@ contract Masset is
}
require(mAssetQuantity > 0, "Must redeem some bAssets");

// Redemption has fee? Fetch the rate
uint256 fee = applyFee ? swapFee : 0;

// Apply fees, burn mAsset and return bAsset to recipient
_settleRedemption(_recipient, mAssetQuantity, props.bAssets, _bAssetQuantities, props.indexes, props.integrators, applyFee);
_settleRedemption(_recipient, mAssetQuantity, props.bAssets, _bAssetQuantities, props.indexes, props.integrators, fee);

emit Redeemed(msg.sender, _recipient, mAssetQuantity, _bAssets, _bAssetQuantities);
return mAssetQuantity;
Expand Down Expand Up @@ -566,7 +573,7 @@ contract Masset is
require(redemptionValid, reason);

// Apply fees, burn mAsset and return bAsset to recipient
_settleRedemption(_recipient, _mAssetQuantity, props.bAssets, bAssetQuantities, props.indexes, props.integrators, false);
_settleRedemption(_recipient, _mAssetQuantity, props.bAssets, bAssetQuantities, props.indexes, props.integrators, redemptionFee);

emit RedeemedMasset(msg.sender, _recipient, _mAssetQuantity);
}
Expand All @@ -579,7 +586,7 @@ contract Masset is
* @param _bAssetQuantities Array of bAsset quantities
* @param _indices Matching indices for the bAsset array
* @param _integrators Matching integrators for the bAsset array
* @param _applyFee Apply a fee to this redemption?
* @param _feeRate Fee rate to be applied to this redemption
*/
function _settleRedemption(
address _recipient,
Expand All @@ -588,25 +595,22 @@ contract Masset is
uint256[] memory _bAssetQuantities,
uint8[] memory _indices,
address[] memory _integrators,
bool _applyFee
uint256 _feeRate
) internal {
// Burn the full amount of Masset
_burn(msg.sender, _mAssetQuantity);

// Reduce the amount of bAssets marked in the vault
basketManager.decreaseVaultBalances(_indices, _integrators, _bAssetQuantities);

// Redemption has fee? Fetch the rate
uint256 fee = _applyFee ? swapFee : 0;

// Transfer the Bassets to the recipient
uint256 bAssetCount = _bAssets.length;
for(uint256 i = 0; i < bAssetCount; i++){
address bAsset = _bAssets[i].addr;
uint256 q = _bAssetQuantities[i];
if(q > 0){
// Deduct the redemption fee, if any
q = _deductSwapFee(bAsset, q, fee);
q = _deductSwapFee(bAsset, q, _feeRate);
// Transfer the Bassets to the user
IPlatformIntegration(_integrators[i]).withdraw(_recipient, bAsset, q, _bAssets[i].isTransferFeeCharged);
}
Expand Down Expand Up @@ -695,6 +699,20 @@ contract Masset is
emit SwapFeeChanged(_swapFee);
}

/**
* @dev Set the ecosystem fee for redeeming a mAsset
* @param _redemptionFee Fee calculated in (%/100 * 1e18)
*/
function setRedemptionFee(uint256 _redemptionFee)
external
onlyGovernor
{
require(_redemptionFee <= MAX_FEE, "Rate must be within bounds");
redemptionFee = _redemptionFee;

emit RedemptionFeeChanged(_redemptionFee);
}

/**
* @dev Gets the address of the BasketManager for this mAsset
* @return basketManager Address
Expand Down
26 changes: 26 additions & 0 deletions test/masset/TestMasset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ contract("Masset", async (accounts) => {
expect(await massetDetails.mAsset.swapFee()).bignumber.eq(
simpleToExactAmount(4, 15),
);
expect(await massetDetails.mAsset.redemptionFee()).bignumber.eq(new BN(0));
expect(await massetDetails.mAsset.decimals()).bignumber.eq(new BN(18));
expect(await massetDetails.mAsset.balanceOf(sa.dummy1)).bignumber.eq(new BN(0));
expect(await massetDetails.mAsset.name()).eq("mStable Mock");
Expand Down Expand Up @@ -126,6 +127,31 @@ contract("Masset", async (accounts) => {
"Rate must be within bounds",
);
});
it("should allow the redemption fee rate to be changed", async () => {
// update by the governor
const oldFee = await massetDetails.mAsset.redemptionFee();
const newfee = simpleToExactAmount(1, 16); // 1%
expect(oldFee).bignumber.not.eq(newfee);
await massetDetails.mAsset.setRedemptionFee(newfee, { from: sa.governor });
expect(await massetDetails.mAsset.redemptionFee()).bignumber.eq(newfee);
// rejected if not governor
await expectRevert(
massetDetails.mAsset.setRedemptionFee(newfee, { from: sa.default }),
"Only governor can execute",
);
// cannot exceed cap
const feeExceedingCap = simpleToExactAmount(11, 16); // 11%
await expectRevert(
massetDetails.mAsset.setRedemptionFee(feeExceedingCap, { from: sa.governor }),
"Rate must be within bounds",
);
// cannot exceed min
const feeExceedingMin = new BN(-1); // 11%
await expectRevert(
massetDetails.mAsset.setRedemptionFee(feeExceedingMin, { from: sa.governor }),
"Rate must be within bounds",
);
});
});

describe("collecting interest", async () => {
Expand Down
97 changes: 96 additions & 1 deletion test/masset/TestMassetRedeemMulti.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ contract("Masset - RedeemMasset", async (accounts) => {
recipient: string = sa.default,
sender: string = sa.default,
ignoreHealthAssertions = false,
expectFee = false,
): Promise<void> => {
const { mAsset, basketManager, bAssets } = md;

Expand Down Expand Up @@ -112,13 +113,40 @@ contract("Masset - RedeemMasset", async (accounts) => {
b.mul(ratioScale).div(new BN(basketComp.bAssets[i].ratio)),
);

let fees = bAssets.map(() => new BN(0));
let feeRate = new BN(0);

// If there is a fee expected, then deduct it from output
if (expectFee) {
feeRate = await mAsset.redemptionFee();
expect(feeRate).bignumber.gt(new BN(0) as any);
expect(feeRate).bignumber.lt(fullScale.div(new BN(50)) as any);
fees = expectedBassetsExact.map((b) => b.mul(feeRate).div(fullScale));
fees.map((f, i) =>
expectedBassetsExact[i].gt(new BN(0) as any)
? expect(f).bignumber.gt(new BN(0) as any)
: null,
);
}

// 4. Validate any basic events that should occur
// Listen for the events
await expectEvent(tx.receipt, "RedeemedMasset", {
redeemer: sender,
recipient,
mAssetQuantity: exactAmount,
});
if (expectFee) {
bAssets.map((b, i) =>
fees[i].gt(new BN(0))
? expectEvent(tx.receipt, "PaidFee", {
payer: sender,
asset: b.address,
feeQuantity: fees[i],
})
: null,
);
}

// 5. Validate output state
// Sender should have less mAsset
Expand All @@ -136,7 +164,8 @@ contract("Masset - RedeemMasset", async (accounts) => {
);
recipientBassetBalsAfter.map((b, i) =>
expect(b).bignumber.eq(
recipientBassetBalsBefore[i].add(expectedBassetsExact[i]),
// Subtract the fee from the returned amount
recipientBassetBalsBefore[i].add(expectedBassetsExact[i]).sub(fees[i]),
`Recipient should have more bAsset[${i}]`,
),
);
Expand All @@ -155,6 +184,7 @@ contract("Masset - RedeemMasset", async (accounts) => {
);
bAssetsAfter.map((b, i) =>
expect(new BN(b.vaultBalance)).bignumber.eq(
// Full amount including fee should be taken from vaultBalance
new BN(basketComp.bAssets[i].vaultBalance).sub(expectedBassetsExact[i]),
`Vault balance should reduce for bAsset[${i}]`,
),
Expand Down Expand Up @@ -220,6 +250,71 @@ contract("Masset - RedeemMasset", async (accounts) => {
);
});
});
context("and there is a non zero redemption fee", async () => {
beforeEach(async () => {
await runSetup(false, false);
// Just mint 100 of everything
await seedWithWeightings(massetDetails, [
new BN(100),
new BN(100),
new BN(100),
new BN(100),
]);
});
it("should take the fee from the redeemed bAssets", async () => {
const { mAsset, bAssets } = massetDetails;
const recipient = sa.dummy1;
const basketComp = await massetMachine.getBasketComposition(massetDetails);

// Set redemption fee to 1%
await mAsset.setRedemptionFee(simpleToExactAmount(1, 16), {
from: sa.governor,
});
const recipientBassetBalsBefore = await Promise.all(
bAssets.map((b) => b.balanceOf(recipient)),
);
const expectedBassetsExact = await Promise.all(
basketComp.bAssets.map((b) =>
simpleToExactAmount(10, 18)
.mul(ratioScale)
.div(new BN(b.ratio)),
),
);
const bAssetFees = expectedBassetsExact.map((b, i) =>
b.mul(simpleToExactAmount(1, 16)).div(fullScale),
);
expect(bAssetFees.reduce((p, c) => p.add(c), new BN(0))).bignumber.gt(
new BN(0) as any,
);

await assertRedemption(
massetDetails,
simpleToExactAmount(40, 18),
recipient,
undefined,
undefined,
true,
);

const recipientBassetBalsAfter = await Promise.all(
bAssets.map((b) => b.balanceOf(recipient)),
);
expectedBassetsExact.map((e, i) =>
expect(recipientBassetBalsAfter[i]).bignumber.eq(
recipientBassetBalsBefore[i]
.add(expectedBassetsExact[i])
.sub(bAssetFees[i]),
),
);

const basketCompAfter = await massetMachine.getBasketComposition(massetDetails);
basketCompAfter.bAssets.map((b, i) =>
expect(b.vaultBalance).bignumber.eq(
basketComp.bAssets[i].vaultBalance.sub(expectedBassetsExact[i]),
),
);
});
});
context("using bAssets with transfer fees", async () => {
beforeEach(async () => {
await runSetup(true, true);
Expand Down