From f26b522b8883599ce9c96a5c04ea1c9ef2e508ba Mon Sep 17 00:00:00 2001 From: Aalavandhan <6264334+aalavandhan@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:49:42 -0500 Subject: [PATCH] Minor router logic update and tests (#261) * updated router behavior to return created vault back to the user after creation * router tests * fixed unstake/restake parameter ordering --- contracts/Mock/MockERC20.sol | 29 +++- contracts/Mock/MockGeyser.sol | 13 +- contracts/Mock/MockVaultFactory.sol | 2 +- contracts/Router/CharmGeyserRouter.sol | 7 +- contracts/Router/GeyserRouter.sol | 45 +++-- frontend/src/sdk/actions.ts | 12 +- test/CharmGeyserRouter.ts | 6 +- test/GeyserRouter.ts | 224 +++++++++++++++++++++++++ 8 files changed, 299 insertions(+), 39 deletions(-) create mode 100644 test/GeyserRouter.ts diff --git a/contracts/Mock/MockERC20.sol b/contracts/Mock/MockERC20.sol index eb2772c9..91a75e40 100644 --- a/contracts/Mock/MockERC20.sol +++ b/contracts/Mock/MockERC20.sol @@ -4,10 +4,37 @@ pragma solidity 0.7.6; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -contract MockERC20 is ERC20 { +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/drafts/IERC20Permit.sol"; + +contract MockERC20 is ERC20, IERC20Permit { + mapping(address => uint256) public override nonces; + constructor(address recipient, uint256 amount) ERC20("MockERC20", "MockERC20") { ERC20._mint(recipient, amount); } + + // Hard-coded DOMAIN_SEPARATOR for testing purposes only. + // In a real implementation, this would be derived from EIP-712 parameters. + function DOMAIN_SEPARATOR() external pure override returns (bytes32) { + return 0x00; + } + + function permit( + address owner, + address spender, + uint256 value, + uint256, // deadline - ignored in mock + uint8, // v - ignored in mock + bytes32, // r - ignored in mock + bytes32 // s - ignored in mock + ) external override { + // For testing, ignore signature checks and deadline. + // Simply set allowance directly. + _approve(owner, spender, value); + // Increment nonce as if a successful permit was used. + nonces[owner]++; + } } contract MockBAL is ERC20 { diff --git a/contracts/Mock/MockGeyser.sol b/contracts/Mock/MockGeyser.sol index 11c18ba8..be0b1542 100644 --- a/contracts/Mock/MockGeyser.sol +++ b/contracts/Mock/MockGeyser.sol @@ -27,13 +27,22 @@ contract MockGeyser { }); } - event LogStaked(address vault, uint256 amount, bytes permission); + event Staked(address vault, uint256 amount, bytes permission); + event UnstakedAndClaimed(address vault, uint256 amount, bytes permission); function stake( address vault, uint256 amount, bytes calldata permission ) external { - emit LogStaked(vault, amount, permission); + emit Staked(vault, amount, permission); + } + + function unstakeAndClaim( + address vault, + uint256 amount, + bytes calldata permission + ) external { + emit UnstakedAndClaimed(vault, amount, permission); } } diff --git a/contracts/Mock/MockVaultFactory.sol b/contracts/Mock/MockVaultFactory.sol index 4dfd24ea..67c5906b 100644 --- a/contracts/Mock/MockVaultFactory.sol +++ b/contracts/Mock/MockVaultFactory.sol @@ -17,7 +17,7 @@ contract MockVaultFactory is ERC721("MockVaultFactory", "MVF") { ) external returns (address) { uint256 vaultId = nextVaultId; nextVaultId++; - _mint(msg.sender, vaultId); + ERC721._safeMint(msg.sender, vaultId); return address(uint160(vaultId)); } } diff --git a/contracts/Router/CharmGeyserRouter.sol b/contracts/Router/CharmGeyserRouter.sol index bc8d9b7a..44b74b1a 100644 --- a/contracts/Router/CharmGeyserRouter.sol +++ b/contracts/Router/CharmGeyserRouter.sol @@ -75,12 +75,7 @@ contract CharmGeyserRouter is GeyserRouter { bytes calldata permission, LiqCreationPayload memory d ) external returns (address vault) { - // create vault - vault = create2Vault(vaultFactory, salt); - - // transfer ownership - IERC721(vaultFactory).safeTransferFrom(address(this), vaultOwner, uint256(vault)); - + vault = create2Vault(vaultFactory, salt, vaultOwner); // create liquidity and stake createLiqAndStake(geyser, vault, permission, d); } diff --git a/contracts/Router/GeyserRouter.sol b/contracts/Router/GeyserRouter.sol index bc4147ea..870a37cf 100644 --- a/contracts/Router/GeyserRouter.sol +++ b/contracts/Router/GeyserRouter.sol @@ -25,8 +25,15 @@ contract GeyserRouter is IERC721Receiver { return IERC721Receiver.onERC721Received.selector; } - function create2Vault(address vaultFactory, bytes32 salt) public returns (address vault) { + function create2Vault( + address vaultFactory, + bytes32 salt, + address vaultOwner + ) public returns (address vault) { + // create vault vault = IFactory(vaultFactory).create2("", salt); + // transfer ownership + IERC721(vaultFactory).safeTransferFrom(address(this), vaultOwner, uint256(vault)); } function depositStake( @@ -48,10 +55,7 @@ contract GeyserRouter is IERC721Receiver { bytes32 salt, bytes calldata permission ) external returns (address vault) { - // create vault - vault = create2Vault(vaultFactory, salt); - // transfer ownership - IERC721(vaultFactory).safeTransferFrom(address(this), vaultOwner, uint256(vault)); + vault = create2Vault(vaultFactory, salt, vaultOwner); // transfer tokens and stake depositStake(geyser, vault, amount, permission); } @@ -108,36 +112,29 @@ contract GeyserRouter is IERC721Receiver { IGeyser(geyser).stake(vault, permit.value, permission); } - struct StakeRequest { + struct GeyserAction { address geyser; address vault; uint256 amount; bytes permission; } - function stakeMulti(StakeRequest[] calldata requests) external { - for (uint256 index = 0; index < requests.length; index++) { - StakeRequest calldata request = requests[index]; - IGeyser(request.geyser).stake(request.vault, request.amount, request.permission); + function stakeMulti(GeyserAction[] calldata actions) external { + for (uint256 index = 0; index < actions.length; index++) { + GeyserAction calldata act = actions[index]; + IGeyser(act.geyser).stake(act.vault, act.amount, act.permission); } } - struct UnstakeRequest { - address geyser; - address vault; - uint256 amount; - bytes permission; - } - - function unstakeMulti(UnstakeRequest[] calldata requests) external { - for (uint256 index = 0; index < requests.length; index++) { - UnstakeRequest calldata request = requests[index]; - IGeyser(request.geyser).unstakeAndClaim(request.vault, request.amount, request.permission); + function unstakeMulti(GeyserAction[] calldata actions) external { + for (uint256 index = 0; index < actions.length; index++) { + GeyserAction calldata act = actions[index]; + IGeyser(act.geyser).unstakeAndClaim(act.vault, act.amount, act.permission); } } - function unstakeAndRestake(UnstakeRequest calldata r1, StakeRequest calldata r2) external { - IGeyser(r1.geyser).stake(r1.vault, r1.amount, r1.permission); - IGeyser(r2.geyser).unstakeAndClaim(r2.vault, r2.amount, r2.permission); + function unstakeAndRestake(GeyserAction calldata unstakeAct, GeyserAction calldata stakeAct) external { + IGeyser(unstakeAct.geyser).unstakeAndClaim(unstakeAct.vault, unstakeAct.amount, unstakeAct.permission); + IGeyser(stakeAct.geyser).stake(stakeAct.vault, stakeAct.amount, stakeAct.permission); } } diff --git a/frontend/src/sdk/actions.ts b/frontend/src/sdk/actions.ts index 95b67a96..f81a8209 100644 --- a/frontend/src/sdk/actions.ts +++ b/frontend/src/sdk/actions.ts @@ -129,7 +129,11 @@ export const approveCreateDepositStake = async (geyserAddress: string, amount: B const token = new Contract(tokenAddress, ERC20_ABI, signer) const salt = randomBytes(32) - const vaultAddress = await router.callStatic.create2Vault(config.VaultFactory.address, salt) + const vaultAddress = await router.callStatic.create2Vault( + config.VaultFactory.address, + salt, + await signer.getAddress(), + ) const vault = new Contract(vaultAddress, config.VaultTemplate.abi, signer) const lockPermission = await signPermission('Lock', vault, signer, geyserAddress, token.address, amount, '0') const args = [geyserAddress, config.VaultFactory.address, await signer.getAddress(), amount, salt, lockPermission] @@ -186,7 +190,11 @@ export const permitCreateDepositStake = async (geyserAddress: string, amount: Bi } const salt = randomBytes(32) - const vaultAddress = await router.callStatic.create2Vault(config.VaultFactory.address, salt) + const vaultAddress = await router.callStatic.create2Vault( + config.VaultFactory.address, + salt, + await signer.getAddress(), + ) const vault = new Contract(vaultAddress, config.VaultTemplate.address, signer) const permit = await signPermitEIP2612(signer, tokenAddress, router.address, amount, deadline) diff --git a/test/CharmGeyserRouter.ts b/test/CharmGeyserRouter.ts index ee902193..9d432e4b 100644 --- a/test/CharmGeyserRouter.ts +++ b/test/CharmGeyserRouter.ts @@ -65,7 +65,7 @@ describe('CharmGeyserRouter', function () { } await expect(router.connect(user).createLiqAndStake(geyser.address, vault, '0x', liqPayload)) - .to.emit(geyser, 'LogStaked') + .to.emit(geyser, 'Staked') .withArgs(vault, token0Amount.add(token1Amount), '0x') // After staking, router should have transferred all tokens out @@ -98,7 +98,7 @@ describe('CharmGeyserRouter', function () { const args = [geyser.address, vaultFactory.address, await vaultOwner.getAddress(), salt, '0x', liqPayload] const vault = await router.connect(user).callStatic.create2VaultCreateLiqAndStake(...args) await expect(router.connect(user).create2VaultCreateLiqAndStake(...args)) - .to.emit(geyser, 'LogStaked') + .to.emit(geyser, 'Staked') .withArgs(vault, token0Amount.add(token1Amount), '0x') expect(await vaultFactory.ownerOf(1)).to.equal(await vaultOwner.getAddress()) }) @@ -145,7 +145,7 @@ describe('CharmGeyserRouter', function () { } await expect(router.connect(user).createLiqAndStake(geyser.address, vault, '0x', liqPayload)) - .to.emit(geyser, 'LogStaked') + .to.emit(geyser, 'Staked') .withArgs(vault, 0, '0x') }) }) diff --git a/test/GeyserRouter.ts b/test/GeyserRouter.ts new file mode 100644 index 00000000..c654aaf7 --- /dev/null +++ b/test/GeyserRouter.ts @@ -0,0 +1,224 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { Signer, Contract, BigNumber } from 'ethers' + +describe('GeyserRouter', function () { + let deployer: Signer + let user: Signer + let vaultOwner: Signer + let router: Contract + let geyser: Contract + let factory: Contract + let stakingToken: Contract + let vaultAddress: string + let vaultNFTId: BigNumber + + beforeEach(async () => { + ;[deployer, user, vaultOwner] = await ethers.getSigners() + + // Mock ERC20 token + const MockERC20 = await ethers.getContractFactory('MockERC20') + stakingToken = await MockERC20.deploy(await user.getAddress(), ethers.utils.parseEther('1000000')) + await stakingToken.deployed() + + // Mock Geyser + const MockGeyser = await ethers.getContractFactory('MockGeyser') + geyser = await MockGeyser.deploy(stakingToken.address) + await geyser.deployed() + + // Mock Factory (ERC721-based) + const MockVaultFactory = await ethers.getContractFactory('MockVaultFactory') + factory = await MockVaultFactory.deploy() + await factory.deployed() + + // Deploy Router + const GeyserRouter = await ethers.getContractFactory('GeyserRouter') + router = await GeyserRouter.deploy() + await router.deployed() + }) + + describe('create2Vault', () => { + it('should create a vault using create2', async () => { + const salt = ethers.utils.formatBytes32String('testSalt') + + vaultAddress = await router.callStatic.create2Vault(factory.address, salt, await vaultOwner.getAddress()) + expect(vaultAddress).to.not.be.undefined + + // The factory should have minted an NFT representing the vault to the vault owner + await router.create2Vault(factory.address, salt, await vaultOwner.getAddress()) + expect(await factory.balanceOf(await vaultOwner.getAddress())).to.equal(1) + }) + }) + + describe('depositStake', () => { + beforeEach(async () => { + // create a vault + const salt = ethers.utils.formatBytes32String('testSalt2') + vaultAddress = await router.callStatic.create2Vault(factory.address, salt, await vaultOwner.getAddress()) + await router.create2Vault(factory.address, salt, await vaultOwner.getAddress()) + }) + + it('should deposit stake tokens from user into vault and call stake on geyser', async () => { + const amount = ethers.utils.parseEther('100') + await stakingToken.connect(user).approve(router.address, amount) + + await expect(router.connect(user).depositStake(geyser.address, vaultAddress, amount, '0x')) + .to.emit(geyser, 'Staked') + .withArgs(vaultAddress, amount, '0x') + + // Check vault (just ensure tokens moved) + expect(await stakingToken.balanceOf(vaultAddress)).to.equal(amount) + }) + }) + + describe('create2VaultAndStake', () => { + it('should create a vault, transfer ownership, and stake tokens', async () => { + const amount = ethers.utils.parseEther('50') + await stakingToken.connect(user).approve(router.address, amount) + const salt = ethers.utils.formatBytes32String('testSalt3') + + const args = [geyser.address, factory.address, await vaultOwner.getAddress(), amount, salt, '0x'] + + const vaultAddr = await router.connect(user).callStatic.create2VaultAndStake(...args) + + await expect(router.connect(user).create2VaultAndStake(...args)) + .to.emit(geyser, 'Staked') + .withArgs(vaultAddr, amount, '0x') + + // vaultOwner should now own the vault NFT + expect(await factory.ownerOf(1)).to.equal(await vaultOwner.getAddress()) + }) + }) + + describe('create2VaultPermitAndStake', () => { + it('should create vault, transfer ownership, permit, and stake', async () => { + const amount = ethers.utils.parseEther('200') + const salt = ethers.utils.formatBytes32String('testSalt4') + + // Mock permit data. In practice, you'd generate a signature off-chain. + // Here, we simply assume the mock ERC20 supports permit or is a mock that doesn't revert. + const deadline = Math.floor(Date.now() / 1000) + 3600 + const owner = await user.getAddress() + const spender = await user.getAddress() + const value = amount + + // Normally you'd produce a real permit signature. For testing we assume mock token doesn't revert. + const v = 27 + const r = ethers.utils.formatBytes32String('r') + const s = ethers.utils.formatBytes32String('s') + + const permit = { + owner, + spender, + value, + deadline, + v, + r, + s, + } + + // user must have tokens + await stakingToken.connect(user).approve(router.address, amount) + + const args = [geyser.address, factory.address, await vaultOwner.getAddress(), salt, permit, '0x'] + const vaultAddr = await router.connect(user).callStatic.create2VaultPermitAndStake(...args) + + await expect(router.connect(user).create2VaultPermitAndStake(...args)) + .to.emit(geyser, 'Staked') + .withArgs(vaultAddr, amount, '0x') + expect(await factory.ownerOf(1)).to.equal(await vaultOwner.getAddress()) + }) + }) + + describe('stakeMulti', () => { + it('should call stake on multiple geysers', async () => { + // create vaults for multiple requests + const salt1 = ethers.utils.formatBytes32String('multi1') + const salt2 = ethers.utils.formatBytes32String('multi2') + const vault1 = await router.callStatic.create2Vault(factory.address, salt1, await vaultOwner.getAddress()) + const vault2 = await router.callStatic.create2Vault(factory.address, salt2, await vaultOwner.getAddress()) + await router.create2Vault(factory.address, salt1, await vaultOwner.getAddress()) + await router.create2Vault(factory.address, salt2, await vaultOwner.getAddress()) + + const amount1 = ethers.utils.parseEther('10') + const amount2 = ethers.utils.parseEther('20') + + const requests = [ + { + geyser: geyser.address, + vault: vault1, + amount: amount1, + permission: '0x', + }, + { + geyser: geyser.address, + vault: vault2, + amount: amount2, + permission: '0x', + }, + ] + + // In a real scenario, you'd need to ensure the vaults have tokens staked beforehand or + // the geyser allows calling `stake` from the router address. For simplicity, we rely on mock logic. + + await expect(router.stakeMulti(requests)) + .to.emit(geyser, 'Staked') + .withArgs(vault1, amount1, '0x') + .and.to.emit(geyser, 'Staked') + .withArgs(vault2, amount2, '0x') + }) + }) + + describe('unstakeMulti', () => { + it('should call unstakeAndClaim on multiple geysers', async () => { + const salt = ethers.utils.formatBytes32String('unstakeTest') + const vault = await router.callStatic.create2Vault(factory.address, salt, await vaultOwner.getAddress()) + await router.create2Vault(factory.address, salt, await vaultOwner.getAddress()) + + const amount = ethers.utils.parseEther('5') + const requests = [ + { + geyser: geyser.address, + vault: vault, + amount: amount, + permission: '0x', + }, + ] + + await expect(router.unstakeMulti(requests)).to.emit(geyser, 'UnstakedAndClaimed').withArgs(vault, amount, '0x') + }) + }) + + describe('unstakeAndRestake', () => { + it('should call stake on first geyser request and unstakeAndClaim on second geyser request', async () => { + const salt1 = ethers.utils.formatBytes32String('r1') + const salt2 = ethers.utils.formatBytes32String('r2') + const vault1 = await router.callStatic.create2Vault(factory.address, salt1, await vaultOwner.getAddress()) + const vault2 = await router.callStatic.create2Vault(factory.address, salt2, await vaultOwner.getAddress()) + await router.create2Vault(factory.address, salt1, await vaultOwner.getAddress()) + await router.create2Vault(factory.address, salt2, await vaultOwner.getAddress()) + + const amount1 = ethers.utils.parseEther('15') + const amount2 = ethers.utils.parseEther('25') + + const r1 = { + geyser: geyser.address, + vault: vault1, + amount: amount1, + permission: '0x', + } + const r2 = { + geyser: geyser.address, + vault: vault2, + amount: amount2, + permission: '0x', + } + + await expect(router.unstakeAndRestake(r1, r2)) + .to.emit(geyser, 'Staked') + .withArgs(vault2, amount2, '0x') + .and.to.emit(geyser, 'UnstakedAndClaimed') + .withArgs(vault1, amount1, '0x') + }) + }) +})