Skip to content

Commit

Permalink
Merge pull request #337 from sushiswap/router-patch
Browse files Browse the repository at this point in the history
Router patch
  • Loading branch information
matthewlilley committed Apr 12, 2022
2 parents ef1f4ed + db7fa6b commit 79e77c2
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 126 deletions.
103 changes: 38 additions & 65 deletions contracts/TridentRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,23 @@ error NotWethSender();
error TooLittleReceived();
error NotEnoughLiquidityMinted();
error IncorrectTokenWithdrawn();
error IncorrectSlippageParams();
error InsufficientWETH();
error InvalidPool();

/// @notice Router contract that helps in swapping across Trident pools.
contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
using Transfer for address;

/// @dev Cached whitelisted pools.
mapping(address => bool) internal whitelistedPools;

/// @notice BentoBox token vault.
IBentoBoxMinimal public immutable bento;

/// @notice Master deployer.
IMasterDeployer public immutable masterDeployer;

/// @notice ERC-20 token for wrapped ETH (v9).
address internal immutable wETH;

/// @notice The user should use 0x0 if they want to use native currency, e.g., ETH.
address constant USE_NATIVE = address(0);

Expand Down Expand Up @@ -78,13 +76,9 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
// The last pool should transfer its output tokens to the user.
// If the user wants to unwrap `wETH`, the final destination should be this contract and
// a batch call should be made to `unwrapWETH`.
for (uint256 i; i < params.path.length; ) {
// We don't necessarily need this check but saving users from themselves.
isWhiteListed(params.path[i].pool);
uint256 n = params.path.length;
for (uint256 i = 0; i < n; i = _increment(i)) {
amountOut = IPool(params.path[i].pool).swap(params.path[i].data);
unchecked {
++i;
}
}
// Ensure that the slippage wasn't too much. This assumes that the pool is honest.
if (amountOut < params.amountOutMinimum) revert TooLittleReceived();
Expand Down Expand Up @@ -115,12 +109,9 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
// Call every pool in the path.
// Pool `N` should transfer its output tokens to pool `N+1` directly.
// The last pool should transfer its output tokens to the user.
for (uint256 i; i < params.path.length; ) {
isWhiteListed(params.path[i].pool);
uint256 n = params.path.length;
for (uint256 i = 0; i < n; i = _increment(i)) {
amountOut = IPool(params.path[i].pool).swap(params.path[i].data);
unchecked {
++i;
}
}
// Ensure that the slippage wasn't too much. This assumes that the pool is honest.
if (amountOut < params.amountOutMinimum) revert TooLittleReceived();
Expand All @@ -134,41 +125,33 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
function complexPath(ComplexPathParams calldata params) public payable {
// Deposit all initial tokens to respective pools and initiate the swaps.
// Input tokens come from the user - output goes to following pools.
for (uint256 i; i < params.initialPath.length; ) {
uint256 n = params.initialPath.length;
for (uint256 i = 0; i < n; i = _increment(i)) {
if (params.initialPath[i].native) {
_depositToBentoBox(params.initialPath[i].tokenIn, params.initialPath[i].pool, params.initialPath[i].amount);
} else {
bento.transfer(params.initialPath[i].tokenIn, msg.sender, params.initialPath[i].pool, params.initialPath[i].amount);
}
isWhiteListed(params.initialPath[i].pool);
IPool(params.initialPath[i].pool).swap(params.initialPath[i].data);
unchecked {
++i;
}
}
// Do all the middle swaps. Input comes from previous pools - output goes to following pools.
for (uint256 i; i < params.percentagePath.length; ) {
// Do all the middle swaps. Input comes from previous pools.
n = params.percentagePath.length;
for (uint256 i = 0; i < n; i = _increment(i)) {
uint256 balanceShares = bento.balanceOf(params.percentagePath[i].tokenIn, address(this));
uint256 transferShares = (balanceShares * params.percentagePath[i].balancePercentage) / uint256(10)**8;
bento.transfer(params.percentagePath[i].tokenIn, address(this), params.percentagePath[i].pool, transferShares);
isWhiteListed(params.percentagePath[i].pool);
IPool(params.percentagePath[i].pool).swap(params.percentagePath[i].data);
unchecked {
++i;
}
}
// Do all the final swaps. Input comes from previous pools - output goes to the user.
for (uint256 i; i < params.output.length; ) {
// Ensure enough was received and transfer the ouput to the recipient.
n = params.output.length;
for (uint256 i = 0; i < n; i = _increment(i)) {
uint256 balanceShares = bento.balanceOf(params.output[i].token, address(this));
if (balanceShares < params.output[i].minAmount) revert TooLittleReceived();
if (params.output[i].unwrapBento) {
bento.withdraw(params.output[i].token, address(this), params.output[i].to, 0, balanceShares);
} else {
bento.transfer(params.output[i].token, address(this), params.output[i].to, balanceShares);
}
unchecked {
++i;
}
}
}

Expand All @@ -183,17 +166,14 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
uint256 minLiquidity,
bytes calldata data
) public payable returns (uint256 liquidity) {
isWhiteListed(pool);
// Send all input tokens to the pool.
for (uint256 i; i < tokenInput.length; ) {
uint256 n = tokenInput.length;
for (uint256 i = 0; i < n; i = _increment(i)) {
if (tokenInput[i].native) {
_depositToBentoBox(tokenInput[i].token, pool, tokenInput[i].amount);
} else {
bento.transfer(tokenInput[i].token, msg.sender, pool, tokenInput[i].amount);
}
unchecked {
++i;
}
}
liquidity = IPool(pool).mint(data);
if (liquidity < minLiquidity) revert NotEnoughLiquidityMinted();
Expand All @@ -210,19 +190,12 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
bytes calldata data,
IPool.TokenAmount[] calldata minWithdrawals
) public {
isWhiteListed(pool);
pool.safeTransferFrom(msg.sender, pool, liquidity);
IPool.TokenAmount[] memory withdrawnLiquidity = IPool(pool).burn(data);
for (uint256 i; i < minWithdrawals.length; ++i) {
uint256 j;
for (; j < withdrawnLiquidity.length; ++j) {
if (withdrawnLiquidity[j].token == minWithdrawals[i].token) {
if (withdrawnLiquidity[j].amount < minWithdrawals[i].amount) revert TooLittleReceived();
break;
}
}
// A token that is present in `minWithdrawals` is missing from `withdrawnLiquidity`.
if (j >= withdrawnLiquidity.length) revert IncorrectTokenWithdrawn();
uint256 n = minWithdrawals.length;
for (uint256 i = 0; i < n; i = _increment(i)) {
if (minWithdrawals[i].token != withdrawnLiquidity[i].token) revert IncorrectSlippageParams();
if (withdrawnLiquidity[i].amount < minWithdrawals[i].amount) revert TooLittleReceived();
}
}

Expand All @@ -238,7 +211,6 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
bytes calldata data,
uint256 minWithdrawal
) public {
isWhiteListed(pool);
// Use 'liquidity = 0' for prefunding.
pool.safeTransferFrom(msg.sender, pool, liquidity);
uint256 withdrawn = IPool(pool).burnSingle(data);
Expand All @@ -250,26 +222,23 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
address token,
uint256 amount,
address recipient,
bool onBento
bool fromBento
) external payable {
if (onBento) {
if (fromBento) {
bento.transfer(token, address(this), recipient, amount);
} else {
token == USE_NATIVE ? recipient.safeTransferETH(address(this).balance) : token.safeTransfer(recipient, amount);
}
}

/// @notice Unwrap this contract's `wETH` into ETH.
function unwrapWETH(uint256 amountMinimum, address recipient) external payable {
/// @notice Unwrap this contract's wETH into ETH.
function unwrapWETH(address recipient) external payable {
uint256 balance = IWETH9(wETH).balanceOf(address(this));
if (balance < amountMinimum) revert InsufficientWETH();
if (balance != 0) {
IWETH9(wETH).withdraw(balance);
recipient.safeTransferETH(balance);
}
IWETH9(wETH).withdraw(balance);
recipient.safeTransferETH(balance);
}

/// @notice Wrapper function to allow pool deployment to be batched.
/// @notice Wrapper function to allow pool deployment to be batched.
function deployPool(address factory, bytes calldata deployData) external payable returns (address) {
return masterDeployer.deployPool(factory, deployData);
}
Expand All @@ -283,6 +252,12 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
bento.setMasterContractApproval(msg.sender, address(this), true, v, r, s);
}

/// @notice Call BentoBox harvest function to rebalance a BentoBox token strategy and ensure there are enough tokens available to withdraw a swap output.
/// @dev Should be batched in before a swap.
function harvest(address token, uint256 maxChangeAmount) external {
bento.harvest(token, true, maxChangeAmount);
}

/// @notice Deposit from the user's wallet into BentoBox.
/// @dev Amount is the native token amount. We let BentoBox do the conversion into shares.
function _depositToBentoBox(
Expand All @@ -292,12 +267,10 @@ contract TridentRouter is ITridentRouter, SelfPermit, Multicall {
) internal {
bento.deposit{value: token == USE_NATIVE ? amount : 0}(token, msg.sender, recipient, amount, 0);
}

/// @notice Check pool whitelisting status.
function isWhiteListed(address pool) internal {
if (!whitelistedPools[pool]) {
if (!masterDeployer.pools(pool)) revert InvalidPool();
whitelistedPools[pool] = true;

function _increment(uint256 i) internal pure returns (uint256) {
unchecked {
return i + 1;
}
}
}
6 changes: 6 additions & 0 deletions contracts/interfaces/IBentoBoxMinimal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,10 @@ interface IBentoBoxMinimal {
bytes32 r,
bytes32 s
) external;

function harvest(
address token,
bool balance,
uint256 maxChangeAmount
) external;
}
73 changes: 12 additions & 61 deletions test/router/TridentRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("Router", function () {
const weth9 = await ethers.getContract<WETH9>("WETH9");
const deployer = await ethers.getNamedSigner("deployer");
await expect(weth9.transfer(router.address, 1)).to.not.be.reverted;
await expect(router.unwrapWETH(1, deployer.address)).to.not.be.reverted;
await expect(router.unwrapWETH(deployer.address)).to.not.be.reverted;
});
it("Reverts when msg.sender is not WETH", async () => {
const router = await ethers.getContract<TridentRouter>("TridentRouter");
Expand Down Expand Up @@ -201,7 +201,7 @@ describe("Router", function () {
});

describe("#burnLiquidity", function () {
it("Reverts when a token present in minWithdrawals is not withdrawn", async () => {
it("Reverts when an incorrect token order for minWithdrawals is sent", async () => {
const deployer = await ethers.getNamedSigner("deployer");

const router = await ethers.getContract<TridentRouter>("TridentRouter");
Expand All @@ -216,27 +216,21 @@ describe("Router", function () {

const token1 = await ethers.getContractAt<ERC20Mock>("ERC20Mock", await pool.token1());

const weth9 = await ethers.getContract("WETH9");

const data = ethers.utils.defaultAbiCoder.encode(["address", "bool"], [deployer.address, false]);

const minWithdrawals = [
{
token: token0.address,
amount: "500000000000000000",
},
{
token: token1.address,
amount: "500000000000000000",
amount: "0",
},
{
token: weth9.address,
amount: "1",
token: token0.address,
amount: "0",
},
];

await expect(router.burnLiquidity(pool.address, balance, data, minWithdrawals)).to.be.revertedWith(
"IncorrectTokenWithdrawn"
"IncorrectSlippageParams"
);
});
it("Reverts when output is less than minimum", async () => {
Expand Down Expand Up @@ -336,22 +330,15 @@ describe("Router", function () {
});

describe("#unwrapWETH", function () {
it("Succeeds if there is enough balance of WETH on router", async () => {
it("Correctly unwraps weth", async () => {
const router = await ethers.getContract<TridentRouter>("TridentRouter");
const weth9 = await ethers.getContract<WETH9>("WETH9");
const deployer = await ethers.getNamedSigner("deployer");
await weth9.transfer(router.address, 1);
await expect(router.unwrapWETH(1, deployer.address)).to.not.be.reverted;
});
it("Reverts if there is not enough balance of WETH on router", async () => {
const router = await ethers.getContract<TridentRouter>("TridentRouter");
const deployer = await ethers.getNamedSigner("deployer");
await expect(router.unwrapWETH(1, deployer.address)).to.be.revertedWith("InsufficientWETH");
});
it("Does nothing if balance is zero and amount is zero", async () => {
const router = await ethers.getContract<TridentRouter>("TridentRouter");
const deployer = await ethers.getNamedSigner("deployer");
await router.unwrapWETH(0, deployer.address);
const difference = await weth9.balanceOf(router.address);
const oldBalance = await ethers.provider.getBalance(ethers.constants.AddressZero);
await expect(router.unwrapWETH(ethers.constants.AddressZero)).to.not.be.reverted;
const newBalance = await ethers.provider.getBalance(ethers.constants.AddressZero);
expect(oldBalance.add(difference).eq(newBalance)).to.be.true;
});
});

Expand Down Expand Up @@ -382,40 +369,4 @@ describe("Router", function () {
await router.approveMasterContract(v, r, s);
});
});

describe("#isWhiteListed", function () {
it("Reverts if the pool is invalid", async () => {
const deployer = await ethers.getNamedSigner("deployer");

const router = await ethers.getContract<TridentRouter>("TridentRouter");

const pool = await uninitializedConstantProductPool();

const token0 = await ethers.getContractAt<ERC20Mock>("ERC20Mock", await pool.token0());

const token1 = await ethers.getContractAt<ERC20Mock>("ERC20Mock", await pool.token1());

let liquidityInput = [
{
token: token0.address,
native: false,
amount: 1,
},
{
token: token1.address,
native: false,
amount: 1,
},
];

await expect(
router.addLiquidity(
liquidityInput,
"0x0000000000000000000000000000000000000000",
1,
ethers.utils.defaultAbiCoder.encode(["address"], [deployer.address])
)
).to.be.revertedWith("InvalidPool");
});
});
});

0 comments on commit 79e77c2

Please sign in to comment.