Skip to content

Commit

Permalink
Redemption fee (#92)
Browse files Browse the repository at this point in the history
* Added redemption fee and test cases

Co-authored-by: Facu Spagnuolo <facuspagnuolo@users.noreply.github.com>
  • Loading branch information
alsco77 and facuspagnuolo committed Jul 14, 2020
1 parent e6357a7 commit c7b3054
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 11 deletions.
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

0 comments on commit c7b3054

Please sign in to comment.