8000 Bulker updates for wrapping/unwrapping native token as base asset and supplying stETH amount by scott-silver · Pull Request #659 · compound-finance/comet · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Bulker updates for wrapping/unwrapping native token as base asset and supplying stETH amount #659

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

Merged
merged 1 commit into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions contracts/bulkers/BaseBulker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ contract BaseBulker {
supplyTo(comet, to, asset, amount);
} else if (action == ACTION_SUPPLY_NATIVE_TOKEN) {
(address comet, address to, uint amount) = abi.decode(data[i], (address, address, uint));
unusedNativeToken -= amount;
supplyNativeTokenTo(comet, to, amount);
uint256 nativeTokenUsed = supplyNativeTokenTo(comet, to, amount);
unusedNativeToken -= nativeTokenUsed;
} else if (action == ACTION_TRANSFER_ASSET) {
(address comet, address to, address asset, uint amount) = abi.decode(data[i], (address, address, address, uint));
transferTo(comet, to, asset, amount);
Expand Down Expand Up @@ -177,11 +177,19 @@ contract BaseBulker {

/**
* @notice Wraps the native token and supplies wrapped native token to a user in Comet
* @return The amount of the native token wrapped and supplied to Comet
* @dev Note: Supports `amount` of `uint256.max` implies max only for base asset
*/
function supplyNativeTokenTo(address comet, address to, uint amount) internal {
IWETH9(wrappedNativeToken).deposit{ value: amount }();
IWETH9(wrappedNativeToken).approve(comet, amount);
CometInterface(comet).supplyFrom(address(this), to, wrappedNativeToken, amount);
function supplyNativeTokenTo(address comet, address to, uint amount) internal returns (uint256) {
uint256 supplyAmount = amount;
if (wrappedNativeToken == CometInterface(comet).baseToken()) {
if (amount == type(uint256).max)
supplyAmount = CometInterface(comet).borrowBalanceOf(msg.sender);
}
IWETH9(wrappedNativeToken).deposit{ value: supplyAmount }();
IWETH9(wrappedNativeToken).approve(comet, supplyAmount);
CometInterface(comet).supplyFrom(address(this), to, wrappedNativeToken, supplyAmount);
return supplyAmount;
}

/**
Expand All @@ -203,11 +211,17 @@ contract BaseBulker {
/**
* @notice Withdraws wrapped native token from Comet, unwraps it to the native token, and transfers it to a user
* @dev Note: This contract must have permission to manage msg.sender's Comet account
* @dev Note: Supports `amount` of `uint256.max` only for the base asset. Should revert for a collateral asset
*/
function withdrawNativeTokenTo(address comet, address to, uint amount) internal {
CometInterface(comet).withdrawFrom(msg.sender, address(this), wrappedNativeToken, amount);
IWETH9(wrappedNativeToken).withdraw(amount);
(bool success, ) = to.call{ value: amount }("");
uint256 withdrawAmount = amount;
if (wrappedNativeToken == CometInterface(comet).baseToken()) {
if (amount == type(uint256).max)
withdrawAmount = CometInterface(comet).balanceOf(msg.sender);
}
CometInterface(comet).withdrawFrom(msg.sender, address(this), wrappedNativeToken, withdrawAmount);
IWETH9(wrappedNativeToken).withdraw(withdrawAmount);
(bool success, ) = to.call{ value: withdrawAmount }("");
if (!success) revert FailedToSendNativeToken();
}

Expand Down
20 changes: 17 additions & 3 deletions contracts/bulkers/MainnetBulker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ contract MainnetBulker is BaseBulker {
/// @notice The action for withdrawing staked ETH from Comet
bytes32 public constant ACTION_WITHDRAW_STETH = "ACTION_WITHDRAW_STETH";

/** Custom errors **/

error UnsupportedBaseAsset();

/**
* @notice Construct a new MainnetBulker instance
* @param admin_ The admin of the Bulker contract
Expand Down Expand Up @@ -59,8 +63,11 @@ contract MainnetBulker is BaseBulker {
/**
* @notice Wraps stETH to wstETH and supplies to a user in Comet
* @dev Note: This contract must have permission to manage msg.sender's Comet account
* @dev Note: wstETH base asset is NOT supported
*/
function supplyStEthTo(address comet, address to, uint stETHAmount) internal {
if (CometInterface(comet).baseToken() == wsteth) revert UnsupportedBaseAsset();

doTransferIn(steth, msg.sender, stETHAmount);
ERC20(steth).approve(wsteth, stETHAmount);
uint wstETHAmount = IWstETH(wsteth).wrap(stETHAmount);
Expand All @@ -71,10 +78,17 @@ contract MainnetBulker is BaseBulker {
/**
* @notice Withdraws wstETH from Comet, unwraps it to stETH, and transfers it to a user
* @dev Note: This contract must have permission to manage msg.sender's Comet account
* @dev Note: wstETH base asset is NOT supported
* @dev Note: Supports `amount` of `uint256.max` to withdraw all wstETH from Comet
*/
function withdrawStEthTo(address comet, address to, uint wstETHAmount) internal {
function withdrawStEthTo(address comet, address to, uint stETHAmount) internal {
if (CometInterface(comet).baseToken() == wsteth) revert UnsupportedBaseAsset();

uint wstETHAmount = stETHAmount == type(uint256).max
? CometInterface(comet).collateralBalanceOf(msg.sender, wsteth)
: IWstETH(wsteth).getWstETHByStETH(stETHAmount);
CometInterface(comet).withdrawFrom(msg.sender, address(this), wsteth, wstETHAmount);
uint stETHAmount = IWstETH(wsteth).unwrap(wstETHAmount);
doTransferOut(steth, to, stETHAmount);
uint unwrappedStETHAmount = IWstETH(wsteth).unwrap(wstETHAmount);
doTransferOut(steth, to, unwrappedStETHAmount);
}
}
69 changes: 57 additions & 12 deletions scenario/MainnetBulkerScenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ scenario(

expect(await stETH.balanceOf(albert.address)).to.be.equal(0n);
expectApproximately(
await comet.collateralBalanceOf(albert.address, wstETH.address),
await wstETH.getWstETHByStETH(toSupplyStEth),
(await comet.collateralBalanceOf(albert.address, wstETH.address)).toBigInt(),
(await wstETH.getWstETHByStETH(toSupplyStEth)).toBigInt(),
1n
);
}
Expand All @@ -64,22 +64,20 @@ scenario(
albert: { $asset1: 2 },
$comet: { $asset1: 5 },
},
cometBalances: {
albert: { $asset1: 1 }
}
},
async ({ comet, actors, bulker }, context) => {
const { albert } = actors;

const stETH = await context.world.deploymentManager.contract('stETH') as ERC20;
const wstETH = await context.world.deploymentManager.contract('wstETH') as IWstETH;

const toWithdrawStEth = exp(1, 18);

// approvals/allowances
await albert.allow(bulker.address, true);
await wstETH.connect(albert.signer).approve(comet.address, toWithdrawStEth);

// supply wstETH
await albert.supplyAsset({asset: wstETH.address, amount: toWithdrawStEth });

// withdraw stETH via bulker
const toWithdrawStEth = (await wstETH.getStETHByWstETH(exp(1, 18))).toBigInt();
const withdrawStEthCalldata = utils.defaultAbiCoder.encode(
['address', 'address', 'uint'],
[comet.address, albert.address, toWithdrawStEth]
Expand All @@ -89,12 +87,59 @@ scenario(

await albert.invoke({ actions, calldata });

// Approximation because some precision will be lost from the stETH to wstETH conversions
expectApproximately(
(await stETH.balanceOf(albert.address)).toBigInt(),
toWithdrawStEth,
2n
);
expectApproximately(
(await comet.collateralBalanceOf(albert.address, wstETH.address)).toBigInt(),
0n,
1n
);
}
);

scenario(
'MainnetBulker > withdraw max stETH leaves no dust',
{
filter: async (ctx) => await isBulkerSupported(ctx) && matchesDeployment(ctx, [{network: 'mainnet', deployment: 'weth'}]),
supplyCaps: {
$asset1: 2,
},
tokenBalances: {
albert: { $asset1: 2 },
$comet: { $asset1: 5 },
},
cometBalances: {
albert: { $asset1: 1 }
}
},
async ({ comet, actors, bulker }, context) => {
const { albert } = actors;

const stETH = await context.world.deploymentManager.contract('stETH') as ERC20;
const wstETH = await context.world.deploymentManager.contract('wstETH') as IWstETH;

await albert.allow(bulker.address, true);

// withdraw max stETH via bulker
const withdrawStEthCalldata = utils.defaultAbiCoder.encode(
['address', 'address', 'uint'],
[comet.address, albert.address, ethers.constants.MaxUint256]
);
const calldata = [withdrawStEthCalldata];
const actions = [await bulker.ACTION_WITHDRAW_STETH()];

await albert.invoke({ actions, calldata });

expectApproximately(
await stETH.balanceOf(albert.address),
await wstETH.getStETHByWstETH(toWithdrawStEth),
(await stETH.balanceOf(albert.address)).toBigInt(),
(await wstETH.getStETHByWstETH(exp(1, 18))).toBigInt(),
1n
);
expect(await comet.collateralBalanceOf(albert.address, wstETH.address)).to.equal(0);
expect(await comet.collateralBalanceOf(albert.address, wstETH.address)).to.be.equal(0n);
}
);

Expand Down
1 change: 1 addition & 0 deletions scenario/constraints/Requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface Requirements {
upgrade?: boolean | object; // Modern constraint
pause?: object; // Pause constraint
utilization?: number; // Utilization constraint
supplyCaps?: object; // Supply cap constraint
}
127 changes: 125 additions & 2 deletions test/bulker-test.ts
7CBF
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,70 @@ describe('bulker', function () {
const supplyAmount = exp(10, 18);
const supplyNativeTokenCalldata = ethers.utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, alice.address, supplyAmount]);
await expect(bulker.connect(alice).invoke([await bulker.ACTION_SUPPLY_NATIVE_TOKEN()], [supplyNativeTokenCalldata], { value: supplyAmount / 2n }))
.to.be.revertedWith('code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)');
.to.be.reverted; // Wrapping ETH to WETH in the Bulker reverts because there is not enough ETH in the txn
});

it('supplyNativeToken with max base', async () => {
const protoco 1E80 l = await makeProtocol({
base: 'WETH',
assets: defaultAssets({}, {
WETH: { factory: await ethers.getContractFactory('FaucetWETH') as FaucetWETH__factory },
USDC: {
supplyCap: exp(100_000, 6)
}
}),
// set rates at 0 to ignore effects of interest rate accrual, which are not relevant to this test
borrowInterestRateBase: 0,
borrowInterestRateSlopeLow: 0,
});
const { comet, tokens: { USDC, WETH }, users: [alice] } = protocol;
const bulkerInfo = await makeBulker({ weth: WETH.address });
F438 const { bulker } = bulkerInfo;

const borrowAmount = exp(10, 18);
const supplyAmount = exp(50_000, 6);
await WETH.allocateTo(comet.address, borrowAmount);
await USDC.allocateTo(alice.address, supplyAmount);

// Alice supplies collateral to borrow the base asset
await USDC.connect(alice).approve(comet.address, supplyAmount);
await comet.connect(alice).supply(USDC.address, supplyAmount);
await comet.connect(alice).withdraw(WETH.address, borrowAmount);

expect(await comet.borrowBalanceOf(alice.address)).to.not.be.equal(0);

// Alice repays max ETH through the bulker
const aliceBalanceBefore = await alice.getBalance();
const borrowBalanceOf = await comet.callStatic.borrowBalanceOf(alice.address);
const supplyNativeTokenCalldata = ethers.utils.defaultAbiCoder.encode(
['address', 'address', 'uint'],
[comet.address, alice.address, ethers.constants.MaxUint256]
);
const txn = await wait(bulker.connect(alice).invoke([await bulker.ACTION_SUPPLY_NATIVE_TOKEN()], [supplyNativeTokenCalldata], { value: borrowAmount * 2n }));
const aliceBalanceAfter = await alice.getBalance();

expect(await comet.borrowBalanceOf(alice.address)).to.be.equal(0);
expect(await ethers.provider.getBalance(bulker.address)).to.be.equal(0); // check extra ETH has been refunded to user
expect(aliceBalanceAfter.sub(aliceBalanceBefore)).to.be.equal(-borrowBalanceOf.toBigInt() - getGasUsed(txn));
});

it('supplyNativeToken with max collateral should revert', async () => {
const protocol = await makeProtocol({
assets: defaultAssets({}, {
WETH: { factory: await ethers.getContractFactory('FaucetWETH') as FaucetWETH__factory }
})
});
const { comet, tokens: { WETH }, users: [alice] } = protocol;
const bulkerInfo = await makeBulker({ weth: WETH.address });
const { bulker } = bulkerInfo;

// No approval is actually needed on the supplyEth action!

// Alice supplies max collateral (doesn't make sense) through the bulker
const supplyNativeTokenCalldata = ethers.utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, alice.address, ethers.constants.MaxUint256]);
await expect(
bulker.connect(alice).invoke([await bulker.ACTION_SUPPLY_NATIVE_TOKEN()], [supplyNativeTokenCalldata], { value: exp(1, 18) })
).to.be.reverted;
});

it('transfer base asset', async () => {
Expand Down Expand Up @@ -272,7 +335,7 @@ describe('bulker', function () {
// Alice gives the Bulker permission over her account
await comet.connect(alice).allow(bulker.address, true);

// Alice supplies 10 ETH through the bulker
// Alice withdraws 10 ETH through the bulker
const aliceBalanceBefore = await alice.getBalance();
const withdrawNativeTokenCalldata = ethers.utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, alice.address, withdrawAmount]);
const txn = await wait(bulker.connect(alice).invoke([await bulker.ACTION_WITHDRAW_NATIVE_TOKEN()], [withdrawNativeTokenCalldata]));
Expand All @@ -282,6 +345,66 @@ describe('bulker', function () {
expect(aliceBalanceAfter.sub(aliceBalanceBefore)).to.be.equal(withdrawAmount - getGasUsed(txn));
});

it('withdrawNativeToken max base', async () => {
const protocol = await makeProtocol({
base: 'WETH',
assets: defaultAssets({}, {
WETH: { factory: await ethers.getContractFactory('FaucetWETH') as FaucetWETH__factory }
})
});
const { comet, tokens: { WETH }, users: [alice], governor } = protocol;
const bulkerInfo = await makeBulker({ weth: WETH.address });
const { bulker } = bulkerInfo;

// Allocate WETH to Comet and Alice's Comet balance
const withdrawAmount = exp(10, 18);
await WETH.allocateTo(alice.address, withdrawAmount);
await governor.sendTransaction({ to: WETH.address, value: withdrawAmount }); // seed WETH contract with ether
await WETH.connect(alice).approve(comet.address, withdrawAmount);
await comet.connect(alice).supply(WETH.address, withdrawAmount);

// Alice gives the Bulker permission over her account
await comet.connect(alice).allow(bulker.address, true);

// Alice withdraws uin256.max ETH through the bulker
const aliceBalanceBefore = await alice.getBalance();
const withdrawNativeTokenCalldata = ethers.utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, alice.address, ethers.constants.MaxUint256]);
const txn = await wait(bulker.connect(alice).invoke([await bulker.ACTION_WITHDRAW_NATIVE_TOKEN()], [withdrawNativeTokenCalldata]));
const aliceBalanceAfter = await alice.getBalance();

expect(await comet.collateralBalanceOf(alice.address, WETH.address)).to.be.equal(0);
expect(aliceBalanceAfter.sub(aliceBalanceBefore)).to.be.equal(withdrawAmount - getGasUsed(txn));
});

it('withdrawNativeToken max collateral reverts', async () => {
const protocol = await makeProtocol({
assets: defaultAssets({}, {
WETH: { factory: await ethers.getContractFactory('FaucetWETH') as FaucetWETH__factory }
})
});
const { comet, tokens: { WETH }, users: [alice], governor } = protocol;
const bulkerInfo = await makeBulker({ weth: WETH.address });
const { bulker } = bulkerInfo;

// Allocate WETH to Comet and Alice's Comet balance
const withdrawAmount = exp(10, 18);
await WETH.allocateTo(alice.address, withdrawAmount);
await governor.sendTransaction({ to: WETH.address, value: withdrawAmount }); // seed WETH contract with ether
await WETH.connect(alice).approve(comet.address, withdrawAmount);
await comet.connect(alice).supply(WETH.address, withdrawAmount);

// Alice gives the Bulker permission over her account
await comet.connect(alice).allow(bulker.address, true);

// Alice withdraws uin256.max ETH through the bulker
const withdrawNativeTokenCalldata = ethers.utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, alice.address, ethers.constants.MaxUint256]);

// ...but it reverts
await expect(
bulker.connect(alice).invoke([await bulker.ACTION_WITHDRAW_NATIVE_TOKEN()], [withdrawNativeTokenCalldata])
).to.be.revertedWithCustomError(comet, 'InvalidUInt128');
});

it('claim rewards', async () => {
const protocol = await makeProtocol({
baseMinForRewards: 10e6,
Expand Down
0