diff --git a/contracts/Ladle.sol b/contracts/Ladle.sol index 905f06bc4..047ca37ab 100644 --- a/contracts/Ladle.sol +++ b/contracts/Ladle.sol @@ -49,14 +49,12 @@ contract Ladle is AccessControl() { } ICauldron public immutable cauldron; - address public poolRouter; mapping (bytes6 => IJoin) public joins; // Join contracts available to manage assets. The same Join can serve multiple assets (ETH-A, ETH-B, etc...) mapping (bytes6 => IPool) public pools; // Pool contracts available to manage series. 12 bytes still free. event JoinAdded(bytes6 indexed assetId, address indexed join); event PoolAdded(bytes6 indexed seriesId, address indexed pool); - event PoolRouterSet(address indexed poolRouter); constructor (ICauldron cauldron_) { cauldron = cauldron_; @@ -123,15 +121,6 @@ contract Ladle is AccessControl() { emit PoolAdded(seriesId, address(pool)); } - /// @dev Set the Pool Router for this Ladle - function setPoolRouter(address poolRouter_) - external - auth - { - poolRouter = poolRouter_; - emit PoolRouterSet(poolRouter_); - } - // ---- Batching ---- @@ -142,8 +131,8 @@ contract Ladle is AccessControl() { Operation[] calldata operations, bytes[] calldata data ) external payable { - require(operations.length == data.length, "Unmatched operation data"); - bytes12 vaultId_; + require(operations.length == data.length, "Mismatched operation data"); + bytes12 cachedId; DataTypes.Vault memory vault; // Execute all operations in the batch. Conditionals ordered by expected frequency. @@ -153,7 +142,7 @@ contract Ladle is AccessControl() { if (operation == Operation.BUILD) { (bytes12 vaultId, bytes6 seriesId, bytes6 ilkId) = abi.decode(data[i], (bytes12, bytes6, bytes6)); - vault = _build(vaultId, seriesId, ilkId); // Cache the vault that was just built + (cachedId, vault) = (vaultId, _build(vaultId, seriesId, ilkId)); // Cache the vault that was just built } else if (operation == Operation.FORWARD_PERMIT) { (bytes6 id, bool asset, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) = @@ -166,18 +155,18 @@ contract Ladle is AccessControl() { } else if (operation == Operation.POUR) { (bytes12 vaultId, address to, int128 ink, int128 art) = abi.decode(data[i], (bytes12, address, int128, int128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _pour(vaultId, vault, to, ink, art); } else if (operation == Operation.SERVE) { (bytes12 vaultId, address to, uint128 ink, uint128 base, uint128 max) = abi.decode(data[i], (bytes12, address, uint128, uint128, uint128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _serve(vaultId, vault, to, ink, base, max); } else if (operation == Operation.ROLL) { (bytes12 vaultId, bytes6 newSeriesId, uint128 max) = abi.decode(data[i], (bytes12, bytes6, uint128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); - (vault,) = _roll(vaultId, vault, newSeriesId, max); // TODO: _roll must return vault and balances + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); + (vault,) = _roll(vaultId, vault, newSeriesId, max); } else if (operation == Operation.FORWARD_DAI_PERMIT) { (bytes6 id, bool asset, address spender, uint256 nonce, uint256 deadline, bool allowed, uint8 v, bytes32 r, bytes32 s) = @@ -202,17 +191,17 @@ contract Ladle is AccessControl() { } else if (operation == Operation.CLOSE) { (bytes12 vaultId, address to, int128 ink, int128 art) = abi.decode(data[i], (bytes12, address, int128, int128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _close(vaultId, vault, to, ink, art); } else if (operation == Operation.REPAY) { (bytes12 vaultId, address to, int128 ink, uint128 min) = abi.decode(data[i], (bytes12, address, int128, uint128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _repay(vaultId, vault, to, ink, min); } else if (operation == Operation.REPAY_VAULT) { (bytes12 vaultId, address to, int128 ink, uint128 max) = abi.decode(data[i], (bytes12, address, int128, uint128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _repayVault(vaultId, vault, to, ink, max); } else if (operation == Operation.TRANSFER_TO_FYTOKEN) { @@ -227,33 +216,33 @@ contract Ladle is AccessControl() { } else if (operation == Operation.STIR_FROM) { (bytes12 vaultId, bytes12 to, uint128 ink, uint128 art) = abi.decode(data[i], (bytes12, bytes12, uint128, uint128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _stirFrom(vaultId, to, ink, art); } else if (operation == Operation.STIR_TO) { (bytes12 from, bytes12 vaultId, uint128 ink, uint128 art) = abi.decode(data[i], (bytes12, bytes12, uint128, uint128)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _stirTo(from, vaultId, ink, art); } else if (operation == Operation.TWEAK) { (bytes12 vaultId, bytes6 seriesId, bytes6 ilkId) = abi.decode(data[i], (bytes12, bytes6, bytes6)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); vault = _tweak(vaultId, seriesId, ilkId); } else if (operation == Operation.GIVE) { (bytes12 vaultId, address to) = abi.decode(data[i], (bytes12, address)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); vault = _give(vaultId, to); delete vault; // Clear the cache, since the vault doesn't necessarily belong to msg.sender anymore + cachedId = bytes12(0); } else if (operation == Operation.DESTROY) { (bytes12 vaultId) = abi.decode(data[i], (bytes12)); - if (vaultId_ != vaultId) vault = getOwnedVault(vaultId); + if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId)); _destroy(vaultId); delete vault; // Clear the cache + cachedId = bytes12(0); - } else { - revert("Invalid operation"); } } } diff --git a/test/064_ladle_stir.ts b/test/064_ladle_stir.ts index c8adbf4c8..bbc234ab3 100644 --- a/test/064_ladle_stir.ts +++ b/test/064_ladle_stir.ts @@ -60,7 +60,7 @@ describe('Ladle - stir', function () { vaultFromId = (env.vaults.get(seriesId) as Map).get(ilkId) as string // ==== Set testing environment ==== - await cauldron.build(owner, vaultToId, seriesId, ilkId) + await ladle.build(vaultToId, seriesId, ilkId) }) it('does not allow moving collateral other than to the origin vault owner', async () => { diff --git a/test/070_ladle_batch.ts b/test/070_ladle_batch.ts index cbc56ca69..5a14163da 100644 --- a/test/070_ladle_batch.ts +++ b/test/070_ladle_batch.ts @@ -36,7 +36,7 @@ describe('Ladle - batch', function () { let weth: WETH9Mock async function fixture() { - return await YieldEnvironment.setup(ownerAcc, [baseId, ilkId], [seriesId]) + return await YieldEnvironment.setup(ownerAcc, [baseId, ilkId, otherIlkId], [seriesId]) } before(async () => { @@ -50,9 +50,11 @@ describe('Ladle - batch', function () { const baseId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) const ilkId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) + const otherIlkId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) const ethId = ethers.utils.formatBytes32String('ETH').slice(0, 14) const seriesId = ethers.utils.hexlify(ethers.utils.randomBytes(6)) const vaultId = ethers.utils.hexlify(ethers.utils.randomBytes(12)) + const otherVaultId = ethers.utils.hexlify(ethers.utils.randomBytes(12)) let ethVaultId: string beforeEach(async () => { @@ -72,7 +74,42 @@ describe('Ladle - batch', function () { ethVaultId = (env.vaults.get(seriesId) as Map).get(ethId) as string }) - it('builds a vault, permit and serve', async () => { + it('operations and their data must match in length', async () => { + await expect(ladle.ladle.batch([0], [])).to.be.revertedWith('Mismatched operation data') + }) + + it('builds a vault, tweaks it and gives it', async () => { + await ladle.batch([ + ladle.buildAction(vaultId, seriesId, ilkId), + ladle.tweakAction(vaultId, seriesId, otherIlkId), + ladle.giveAction(vaultId, other), + ]) + }) + + it('builds two vaults and gives them', async () => { + await ladle.batch([ + ladle.buildAction(vaultId, seriesId, ilkId), + ladle.giveAction(vaultId, other), + ladle.buildAction(otherVaultId, seriesId, ilkId), + ladle.giveAction(otherVaultId, other), + ]) + }) + + it('builds a vault and destroys it', async () => { + await ladle.batch([ladle.buildAction(vaultId, seriesId, ilkId), ladle.destroyAction(vaultId)]) + }) + + it("after giving a vault, it can't tweak it", async () => { + await expect( + ladle.batch([ + ladle.buildAction(vaultId, seriesId, ilkId), + ladle.giveAction(vaultId, other), + ladle.tweakAction(vaultId, seriesId, otherIlkId), + ]) + ).to.be.revertedWith('Only vault owner') + }) + + it('builds a vault, permit and pour', async () => { const ilkSeparator = await ilk.DOMAIN_SEPARATOR() const deadline = MAX const posted = WAD.mul(2) @@ -90,7 +127,7 @@ describe('Ladle - batch', function () { await ladle.batch([ ladle.buildAction(vaultId, seriesId, ilkId), ladle.forwardPermitAction(ilkId, true, ilkJoin.address, posted, deadline, v, r, s), - ladle.serveAction(vaultId, owner, posted, borrowed, MAX), + ladle.pourAction(vaultId, owner, posted, borrowed), ]) const vault = await cauldron.vaults(vaultId) @@ -119,7 +156,7 @@ describe('Ladle - batch', function () { expect(vault.ilkId).to.equal(ethId) }) - it('users can transfer ETH then pour, then serve in a single transaction with multicall', async () => { + it('users can transfer ETH then pour, then serve', async () => { const posted = WAD.mul(2) const borrowed = WAD @@ -133,8 +170,73 @@ describe('Ladle - batch', function () { ) }) + it('users can transfer ETH then pour, then close', async () => { + const posted = WAD.mul(2) + const borrowed = WAD + + await ladle.batch( + [ + ladle.joinEtherAction(ethId), + ladle.pourAction(ethVaultId, owner, posted, borrowed), + ladle.closeAction(ethVaultId, other, 0, borrowed.div(2).mul(-1)), + ], + { value: posted } + ) + }) + + it('users can transfer to a pool and repay in a batch', async () => { + const separator = await base.DOMAIN_SEPARATOR() + const deadline = MAX + const amount = WAD + const nonce = await base.nonces(owner) + const approval = { + owner: owner, + spender: ladle.address, + value: amount, + } + const permitDigest = signatures.getPermitDigest(separator, approval, nonce, deadline) + + const { v, r, s } = signatures.sign(permitDigest, signatures.privateKey0) + + const posted = WAD.mul(2) + const borrowed = WAD + + await ladle.batch([ + ladle.buildAction(vaultId, seriesId, ilkId), + ladle.pourAction(vaultId, owner, posted, borrowed), + ladle.forwardPermitAction(baseId, true, ladle.address, amount, deadline, v, r, s), + ladle.transferToPoolAction(seriesId, true, WAD.div(2)), + ladle.repayAction(vaultId, other, 0, 0), + ]) + }) + + it('users can transfer to a pool and repay a whole vault in a batch', async () => { + const separator = await base.DOMAIN_SEPARATOR() + const deadline = MAX + const amount = WAD + const nonce = await base.nonces(owner) + const approval = { + owner: owner, + spender: ladle.address, + value: amount, + } + const permitDigest = signatures.getPermitDigest(separator, approval, nonce, deadline) + + const { v, r, s } = signatures.sign(permitDigest, signatures.privateKey0) + + const posted = WAD.mul(2) + const borrowed = WAD + + await ladle.batch([ + ladle.buildAction(vaultId, seriesId, ilkId), + ladle.pourAction(vaultId, owner, posted, borrowed), + ladle.forwardPermitAction(baseId, true, ladle.address, amount, deadline, v, r, s), + ladle.transferToPoolAction(seriesId, true, WAD), + ladle.repayVaultAction(vaultId, other, 0, MAX), + ]) + }) + it('calls can be routed to pools', async () => { - await ladle.build(vaultId, seriesId, ilkId) await base.mint(pool.address, WAD) const retrieveBaseTokenCall = pool.interface.encodeFunctionData('retrieveBaseToken', [owner]) @@ -142,4 +244,11 @@ describe('Ladle - batch', function () { .to.emit(base, 'Transfer') .withArgs(pool.address, owner, WAD) }) + + it('errors bubble up from calls routed to pools', async () => { + await base.mint(pool.address, WAD) + + const sellBaseTokenCall = pool.interface.encodeFunctionData('sellBaseToken', [owner, MAX128]) + await expect(ladle.route(seriesId, sellBaseTokenCall)).to.be.revertedWith('Pool: Not enough fyToken obtained') + }) }) diff --git a/test/081_witch.ts b/test/081_witch.ts index 6d91d438e..af5249c79 100644 --- a/test/081_witch.ts +++ b/test/081_witch.ts @@ -109,6 +109,12 @@ describe('Witch', function () { await expect(witch.buy(vaultId, WAD, WAD)).to.be.revertedWith('Not enough bought') }) + it('it can buy no collateral (coverage)', async () => { + expect(await witch.buy(vaultId, 0, 0)) + .to.emit(witch, 'Bought') + .withArgs(owner, vaultId, 0, 0) + }) + it('allows to buy 1/2 of the collateral for the whole debt at the beginning', async () => { const baseBalanceBefore = await base.balanceOf(owner) const ilkBalanceBefore = await ilk.balanceOf(owner)