Skip to content

Commit

Permalink
withdrawReserves (compound-finance#141)
Browse files Browse the repository at this point in the history
* withdrawReserves, tests, scenarios

* update scenario

* update unit tests

* simplify governor withdraws scenario

* confirm albert baseToken balance

* update require message

* do transfer out

* delete ReservesWithdrawn event

* confirm comet balance in unit test

* insufficient reserves check

* additional unit test

* inline reserves

* typo
  • Loading branch information
scott-silver authored Feb 8, 2022
1 parent df8a12f commit a6588a8
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 31 deletions.
11 changes: 11 additions & 0 deletions contracts/Comet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1382,4 +1382,15 @@ contract Comet is CometMath, CometStorage {
uint assetWeiPerUnitBase = assetInfo.scale * basePrice / assetPrice;
return assetWeiPerUnitBase * baseAmount / baseScale;
}

/**
* @notice Withdraws base token reserves if called by the governor
* @param to An address of the receiver of withdrawn reserves
* @param amount The amount of reserves to be withdrawn from the protocol
*/
function withdrawReserves(address to, uint amount) external {
require(msg.sender == governor, "only governor may withdraw");
require(amount <= unsigned256(getReserves()), "insufficient reserves");
doTransferOut(baseToken, to, amount);
}
}
58 changes: 58 additions & 0 deletions scenario/WithdrawReservesScenario.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { scenario } from './context/CometContext';
import { expect } from 'chai';

scenario(
'Comet#withdrawReserves > governor withdraw reserves',
{
baseToken: {
balance: 100,
},
upgrade: true,
},
async ({ comet, actors, getAssetByAddress }) => {
const { admin, albert } = actors;

const baseToken = getAssetByAddress(await comet.baseToken());

expect(await baseToken.balanceOf(comet.address)).to.equal(100n);

await admin.withdrawReserves(albert, 10);

expect(await baseToken.balanceOf(comet.address)).to.equal(90n);
expect(await baseToken.balanceOf(albert.address)).to.equal(10n);
}
);

scenario(
'Comet#withdrawReserves > reverts if not called by governor',
{
baseToken: {
balance: 100,
},
upgrade: true,
},
async ({ actors }) => {
const { albert } = actors;
await expect(albert.withdrawReserves(albert, 10)).to.be.revertedWith(
'only governor may withdraw'
);
}
);

scenario(
'Comet#withdrawReserves > reverts if not enough reserves are owned by protocol',
{
baseToken: {
balance: 100,
},
upgrade: true,
},
async ({ actors }) => {
const { admin, albert } = actors;

await expect(admin.withdrawReserves(albert, 101)).to.be.revertedWith('insufficient reserves');
}
);

// XXX add scenario that tests for a revert when reserves are reduced by
// totalSupplyBase
36 changes: 36 additions & 0 deletions scenario/constraints/BaseTokenProtocolBalanceConstraint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Constraint, World } from '../../plugins/scenario';
import { CometContext } from '../context/CometContext';
import { expect } from 'chai';

export class BaseTokenProtocolBalanceConstraint<T extends CometContext> implements Constraint<T> {
async solve(requirements: object, context: T, world: World) {
const baseTokenRequirements = requirements['baseToken'];
if (!baseTokenRequirements) {
return null;
}
if (typeof baseTokenRequirements['balance'] !== 'undefined') {
return async (context: CometContext) => {
const { comet } = context;
const amount = baseTokenRequirements['balance'];
const baseToken = context.getAssetByAddress(await comet.baseToken());

await context.sourceTokens(world, amount, baseToken, comet.address);
};
}
}

async check(requirements: object, context: T, world: World) {
const baseTokenRequirements = requirements['baseToken'];
if (!baseTokenRequirements) {
return null;
}
if (typeof baseTokenRequirements['balance'] !== 'undefined') {
const amount = baseTokenRequirements['balance'];
const { comet } = context;

const baseToken = context.getAssetByAddress(await comet.baseToken());

expect(await baseToken.balanceOf(comet.address)).to.equal(BigInt(amount));
}
}
}
1 change: 1 addition & 0 deletions scenario/constraints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { PauseConstraint } from '../constraints/PauseConstraint';
export { RemoteTokenConstraint } from '../constraints/RemoteTokenConstraint';
export { ModernConstraint } from '../constraints/ModernConstraint';
export { UtilizationConstraint } from '../constraints/UtilizationConstraint';
export { BaseTokenProtocolBalanceConstraint } from './BaseTokenProtocolBalanceConstraint';
35 changes: 20 additions & 15 deletions scenario/context/CometActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ export default class CometActor {
context: CometContext;
info: object;

constructor(name: string, signer: SignerWithAddress, address: string, context: CometContext, info: object = {}) {
constructor(
name: string,
signer: SignerWithAddress,
address: string,
context: CometContext,
info: object = {}
) {
this.name = name;
this.signer = signer;
this.address = address;
Expand All @@ -41,7 +47,7 @@ export default class CometActor {
async sendEth(recipient: AddressLike, amount: number) {
let tx = await this.signer.sendTransaction({
to: resolveAddress(recipient),
value: floor(amount * 1e18)
value: floor(amount * 1e18),
});
await tx.wait();
}
Expand All @@ -64,21 +70,14 @@ export default class CometActor {
).wait();
}

async transfer({
dst,
asset,
amount,
}) {
await (await this.context.comet.connect(this.signer).transfer(dst, asset, amount)).wait()
async transfer({ dst, asset, amount }) {
await (await this.context.comet.connect(this.signer).transfer(dst, asset, amount)).wait();
}

async transferFrom({
src,
dst,
asset,
amount,
}) {
await (await this.context.comet.connect(this.signer).transferFrom(src, dst, asset, amount)).wait()
async transferFrom({ src, dst, asset, amount }) {
await (
await this.context.comet.connect(this.signer).transferFrom(src, dst, asset, amount)
).wait();
}

async signAuthorization({
Expand Down Expand Up @@ -134,4 +133,10 @@ export default class CometActor {
async show() {
return console.log(`Actor#${this.name}{${JSON.stringify(this.info)}}`);
}

async withdrawReserves(to: CometActor, amount: number) {
await (
await this.context.comet.connect(this.signer).withdrawReserves(to.address, amount)
).wait();
}
}
42 changes: 26 additions & 16 deletions scenario/context/CometContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PauseConstraint,
RemoteTokenConstraint,
UtilizationConstraint,
BaseTokenProtocolBalanceConstraint,
} from '../constraints';
import CometActor from './CometActor';
import CometAsset from './CometAsset';
Expand All @@ -24,18 +25,15 @@ export class CometContext {
comet: Comet;
proxyAdmin: ProxyAdmin;

constructor(
deploymentManager: DeploymentManager,
comet: Comet,
proxyAdmin: ProxyAdmin
) {
constructor(deploymentManager: DeploymentManager, comet: Comet, proxyAdmin: ProxyAdmin) {
this.deploymentManager = deploymentManager;
this.comet = comet;
this.proxyAdmin = proxyAdmin;
}

private debug(...args: any[]) {
if (true) { // debug if?
if (true) {
// debug if?
if (typeof args[0] === 'function') {
console.log(...args[0]());
} else {
Expand Down Expand Up @@ -97,9 +95,14 @@ export class CometContext {
throw new Error(`Unable to find asset by address ${address}`);
}

async sourceTokens(world: World, amount: number | bigint, asset: CometAsset | string, recipient: AddressLike) {
async sourceTokens(
world: World,
amount: number | bigint,
asset: CometAsset | string,
recipient: AddressLike
) {
let recipientAddress = resolveAddress(recipient);
let cometAsset = typeof(asset) === 'string' ? this.getAssetByAddress(asset) : asset;
let cometAsset = typeof asset === 'string' ? this.getAssetByAddress(asset) : asset;

// First, try to steal from a known actor
for (let [name, actor] of Object.entries(this.actors)) {
Expand All @@ -114,12 +117,18 @@ export class CometContext {
if (world.isForked()) {
throw new Error('Tokens cannot be sourced from Etherscan for development. Actors did not have sufficient assets.');
} else {
this.debug("Source Tokens: sourcing from Etherscan...");
this.debug('Source Tokens: sourcing from Etherscan...');
// TODO: Note, this never gets called right now since all tokens are faucet tokens we've created.
await sourceTokens({hre: this.deploymentManager.hre, amount, asset: cometAsset.address, address: recipientAddress});
await sourceTokens({
hre: this.deploymentManager.hre,
amount,
asset: cometAsset.address,
address: recipientAddress,
});
}
}

// <!--
async setAssets() {
let signer = (await this.deploymentManager.hre.ethers.getSigners())[1]; // dunno?
let numAssets = await this.comet.numAssets();
Expand Down Expand Up @@ -175,12 +184,12 @@ const getInitialContext = async (world: World): Promise<CometContext> => {
let context = new CometContext(deploymentManager, comet, proxyAdmin);

context.actors = {
admin: await buildActor("admin", adminSigner, context),
pauseGuardian: await buildActor("pauseGuardian", pauseGuardianSigner, context),
albert: await buildActor("albert", albertSigner, context),
betty: await buildActor("betty", bettySigner, context),
charles: await buildActor("charles", charlesSigner, context),
signer: await buildActor("signer", localAdminSigner, context),
admin: await buildActor('admin', adminSigner, context),
pauseGuardian: await buildActor('pauseGuardian', pauseGuardianSigner, context),
albert: await buildActor('albert', albertSigner, context),
betty: await buildActor('betty', bettySigner, context),
charles: await buildActor('charles', charlesSigner, context),
signer: await buildActor('signer', localAdminSigner, context),
};

await context.setAssets();
Expand All @@ -198,6 +207,7 @@ export const constraints = [
new BalanceConstraint(),
new RemoteTokenConstraint(),
new UtilizationConstraint(),
new BaseTokenProtocolBalanceConstraint(),
];

export const scenario = buildScenarioFn<CometContext>(getInitialContext, forkContext, constraints);
13 changes: 13 additions & 0 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type ProtocolOpts = {
baseMinForRewards?: Numeric;
baseBorrowMin?: Numeric;
targetReserves?: Numeric;
baseTokenBalance?: Numeric;
};

export type Protocol = {
Expand Down Expand Up @@ -210,6 +211,12 @@ export async function makeProtocol(opts: ProtocolOpts = {}): Promise<Protocol> {
});
await comet.deployed();

const baseTokenBalance = opts.baseTokenBalance;
if (baseTokenBalance) {
const baseToken = tokens[base];
await wait(baseToken.allocateTo(comet.address, baseTokenBalance));
}

return {
opts,
governor,
Expand Down Expand Up @@ -250,3 +257,9 @@ export async function wait(
receipt,
};
}

export function filterEvent(data, eventName) {
return data.receipt.events?.filter((x) => {
return x.event == eventName;
})[0];
}
69 changes: 69 additions & 0 deletions test/withdraw-reserves-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect, makeProtocol, wait, filterEvent } from './helpers';

describe('withdrawReserves', function () {
it('withdraws reserves from the protocol', async () => {
const tokenBalance = 1000;
const {
comet,
tokens: { USDC },
users: [alice],
governor,
} = await makeProtocol({
baseTokenBalance: tokenBalance,
});

expect(await USDC.balanceOf(alice.address)).to.be.equal(0);
await comet.connect(governor).withdrawReserves(alice.address, tokenBalance);
expect(await USDC.balanceOf(alice.address)).to.equal(tokenBalance);
expect(await USDC.balanceOf(comet.address)).to.equal(0);
});

it('reverts if called not by governor', async () => {
const {
comet,
users: [alice],
} = await makeProtocol();
await expect(comet.connect(alice).withdrawReserves(alice.address, 10)).to.be.revertedWith(
'only governor may withdraw'
);
});

it('reverts if not enough reserves are owned by protocol', async () => {
const tokenBalance = 1000;
const {
comet,
governor,
users: [alice],
} = await makeProtocol({
baseTokenBalance: tokenBalance,
});
await expect(
comet.connect(governor).withdrawReserves(alice.address, tokenBalance + 1)
).to.be.revertedWith('insufficient reserves');
});

it('accounts for total supply base when calculating reserves', async () => {
const {
comet,
governor,
users: [alice],
} = await makeProtocol({
baseTokenBalance: 200,
});

const totalsBasic = await comet.totalsBasic();

await wait(
comet.setTotalsBasic({
...totalsBasic,
totalSupplyBase: 100n,
})
);

expect(await comet.getReserves()).to.be.equal(100);

await expect(comet.connect(governor).withdrawReserves(alice.address, 101)).to.be.revertedWith(
'insufficient reserves'
);
});
});

0 comments on commit a6588a8

Please sign in to comment.