From ac5b1eab11d055c88e31121fe45d4e1896e0773f Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Mon, 9 Aug 2021 20:33:16 -0700 Subject: [PATCH] fix: donate and swap tests now pass with GrantRoundManager update --- contracts/contracts/GrantRoundManager.sol | 34 ++++++++++--- contracts/test/GrantRoundManager.test.ts | 61 ++++++++++++++--------- types/src/grants.d.ts | 20 +++++--- 3 files changed, 77 insertions(+), 38 deletions(-) diff --git a/contracts/contracts/GrantRoundManager.sol b/contracts/contracts/GrantRoundManager.sol index 845647e7..8669b4fc 100644 --- a/contracts/contracts/GrantRoundManager.sol +++ b/contracts/contracts/GrantRoundManager.sol @@ -28,17 +28,17 @@ contract GrantRoundManager { mapping(IERC20 => uint256) internal swapOutputs; /// @notice WETH address - address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + IERC20 public constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); /// @notice Scale factor uint256 internal constant WAD = 1e18; + /// --- Types --- /// @notice Defines the total `amount` of the specified `token` that needs to be swapped to `donationToken`. If /// `path == donationToken`, no swap is required and we just transfer the tokens struct SwapSummary { - address token; - uint256 amount; - uint256 amountOutMinimum; // minimum amount to be returned after swap + uint256 amountIn; + uint256 amountOutMin; // minimum amount to be returned after swap bytes path; } @@ -63,13 +63,24 @@ contract GrantRoundManager { ISwapRouter _router, IERC20 _donationToken ) { + // Validation require(_registry.grantCount() >= 0, "GrantRoundManager: Invalid registry"); require(address(_router).isContract(), "GrantRoundManager: Invalid router"); // Router interface doesn't have a state variable to check require(_donationToken.totalSupply() > 0, "GrantRoundManager: Invalid token"); + // Set state registry = _registry; router = _router; donationToken = _donationToken; + + // Token approvals of common tokens + // TODO inherit from SwapRouter to remove the need for this approvals and extra safeTransferFrom before swap + IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F).safeApprove(address(_router), type(uint256).max); // DAI + IERC20(0xDe30da39c46104798bB5aA3fe8B9e0e1F348163F).safeApprove(address(_router), type(uint256).max); // GTC + IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48).safeApprove(address(_router), type(uint256).max); // USDC + IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7).safeApprove(address(_router), type(uint256).max); // USDT + IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599).safeApprove(address(_router), type(uint256).max); // WBTC + IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2).safeApprove(address(_router), type(uint256).max); // WETH } // --- Core methods --- @@ -146,15 +157,24 @@ contract GrantRoundManager { for (uint256 i = 0; i < _swaps.length; i++) { // Do nothing if the swap input token equals donationToken IERC20 _tokenIn = IERC20(_swaps[i].path.toAddress(0)); - if (_tokenIn == donationToken) continue; + if (_tokenIn == donationToken) { + swapOutputs[_tokenIn] = _swaps[i].amountIn; + continue; + } + + // Transfer input token to this contract if required + // TODO inherit from SwapRouter to remove the need for this + if (_tokenIn != WETH || msg.value == 0) { + _tokenIn.safeTransferFrom(msg.sender, address(this), _swaps[i].amountIn); + } // Otherwise, execute swap ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams( _swaps[i].path, address(this), // send output to the contract and it will be transferred later _deadline, - _swaps[i].amount, - _swaps[i].amountOutMinimum + _swaps[i].amountIn, + _swaps[i].amountOutMin ); require(swapOutputs[_tokenIn] == 0, "GrantRoundManager: Swap parameter has duplicate input tokens"); diff --git a/contracts/test/GrantRoundManager.test.ts b/contracts/test/GrantRoundManager.test.ts index 6bf4126a..39f62bcd 100644 --- a/contracts/test/GrantRoundManager.test.ts +++ b/contracts/test/GrantRoundManager.test.ts @@ -10,7 +10,7 @@ import { expect } from 'chai'; // --- Our imports --- import { ETH_ADDRESS, UNISWAP_ROUTER, tokens, approve, balanceOf, setBalance, encodeRoute } from './utils'; // prettier-ignore import { GrantRoundManager } from '../typechain'; -import { Donation } from '@dgrants/types'; +import { SwapSummary, Donation } from '@dgrants/types'; // --- Parse and define helpers --- const { isAddress, parseUnits } = ethers.utils; @@ -153,11 +153,12 @@ describe('GrantRoundManager', () => { }); }); - describe('swapAndDonate', () => { + describe('donate', () => { let mockRound: MockContract; + let swap: SwapSummary; let donation: Donation; let payee: string; // address the grant owner receives donations to - const farTimestamp = '10000000000'; // date of 2286-11-20 + const deadline = '10000000000'; // date of 2286-11-20 as swap deadline beforeEach(async () => { // Deploy a mock GrantRound @@ -170,13 +171,17 @@ describe('GrantRoundManager', () => { await mockRegistry.mock.getGrantPayee.returns(payee); // Configure default donation data + swap = { + amountIn: '1', + amountOutMin: '0', + path: await encodeRoute(['dai', 'eth', 'gtc']), + }; + donation = { grantId: 0, + token: tokens.dai.address, + ratio: parseUnits('1', 18), rounds: [mockRound.address], - path: await encodeRoute(['dai', 'eth', 'gtc']), - deadline: farTimestamp, // arbitrary date far in the future - amountIn: '1', - amountOutMinimum: '0', }; // Fund the first user with tokens @@ -186,73 +191,81 @@ describe('GrantRoundManager', () => { }); it('reverts if no rounds are specified', async () => { - await expect(manager.swapAndDonate({ ...donation, rounds: [] })).to.be.revertedWith( + await expect(manager.donate([swap], deadline, [{ ...donation, rounds: [] }])).to.be.revertedWith( 'GrantRoundManager: Must specify at least one round' ); }); it('reverts if an invalid grant ID is provided', async () => { - await expect(manager.swapAndDonate({ ...donation, grantId: '500' })).to.be.revertedWith( + await expect(manager.donate([swap], deadline, [{ ...donation, grantId: '500' }])).to.be.revertedWith( 'GrantRoundManager: Grant does not exist in registry' ); }); it('reverts if a provided grant round has a different donation token than the GrantRoundManager', async () => { await mockRound.mock.donationToken.returns(ETH_ADDRESS); - await expect(manager.swapAndDonate(donation)).to.be.revertedWith( + await expect(manager.donate([swap], deadline, [{ ...donation }])).to.be.revertedWith( "GrantRoundManager: GrantRound's donation token does not match GrantRoundManager's donation token" ); }); it('reverts if a provided grant round is not active', async () => { await mockRound.mock.isActive.returns(false); - await expect(manager.swapAndDonate(donation)).to.be.revertedWith('GrantRoundManager: GrantRound is not active'); + await expect(manager.donate([swap], deadline, [{ ...donation }])).to.be.revertedWith( + 'GrantRoundManager: GrantRound is not active' + ); }); it('input token GTC, output token GTC', async () => { const amountIn = parseUnits('100', 18); expect(await balanceOf('gtc', payee)).to.equal('0'); await approve('gtc', user, manager.address); - await manager.swapAndDonate({ ...donation, path: tokens.gtc.address, amountIn }); + await manager.donate([{ ...swap, path: tokens.gtc.address, amountIn }], deadline, [ + { ...donation, token: tokens.gtc.address }, + ]); expect(await balanceOf('gtc', payee)).to.equal(amountIn); }); it('input token ETH, output token GTC', async () => { // Use the 1% GTC/ETH pool to swap from ETH (input) to GTC (donationToken). The 1% pool is currently the most liquid const amountIn = parseUnits('10', 18); - const tx = await manager.swapAndDonate( - { ...donation, path: await encodeRoute(['eth', 'gtc']), amountIn }, + const tx = await manager.donate( + [{ ...swap, amountIn, path: await encodeRoute(['eth', 'gtc']) }], + deadline, + [{ ...donation, token: tokens.weth.address }], { value: amountIn, gasPrice: '0' } // zero gas price to make balance checks simpler ); - // Get the amountOut from the swap from the GrantDonation log + // Get the donationAmount from the swap from the GrantDonation log const receipt = await ethers.provider.getTransactionReceipt(tx.hash); const log = manager.interface.parseLog(receipt.logs[receipt.logs.length - 1]); // the event we want is the last one - const { amountOut } = log.args; - expect(await balanceOf('gtc', payee)).to.equal(amountOut); + const { donationAmount } = log.args; + expect(await balanceOf('gtc', payee)).to.equal(donationAmount); }); it('input token DAI, output token GTC, swap passes through ETH', async () => { // Execute donation to the payee const amountIn = parseUnits('100', 18); await approve('dai', user, manager.address); - const tx = await manager.swapAndDonate({ ...donation, path: await encodeRoute(['dai', 'eth', 'gtc']), amountIn }); + const tx = await manager.donate([{ ...swap, amountIn }], deadline, [donation]); - // Get the amountOut from the swap from the GrantDonation log + // Get the donationAmount from the swap from the GrantDonation log const receipt = await ethers.provider.getTransactionReceipt(tx.hash); const log = manager.interface.parseLog(receipt.logs[receipt.logs.length - 1]); // the event we want is the last one - const { amountOut } = log.args; - expect(await balanceOf('gtc', payee)).to.equal(amountOut); + const { donationAmount } = log.args; + expect(await balanceOf('gtc', payee)).to.equal(donationAmount); }); it('emits a log on a successful donation', async () => { - // Execute donation to the payee + // Execute donation to the payee using GTC as input const amountIn = parseUnits('100', 18); await approve('gtc', user, manager.address); - const tx = await manager.swapAndDonate({ ...donation, path: tokens.gtc.address, amountIn }); + const tx = await manager.donate([{ ...swap, path: tokens.gtc.address, amountIn }], deadline, [ + { ...donation, token: tokens.gtc.address }, + ]); await expect(tx) .to.emit(manager, 'GrantDonation') - .withArgs('0', tokens.gtc.address, amountIn, amountIn, [mockRound.address]); + .withArgs('0', tokens.gtc.address, amountIn, [mockRound.address]); }); }); }); diff --git a/types/src/grants.d.ts b/types/src/grants.d.ts index 33e9bf6f..6f1cc6fa 100644 --- a/types/src/grants.d.ts +++ b/types/src/grants.d.ts @@ -1,4 +1,4 @@ -import { BigNumber, BigNumberish } from 'ethers'; +import { BigNumber, BigNumberish, BytesLike } from 'ethers'; import { TokenInfo } from '@uniswap/token-lists'; // --- Types --- @@ -14,15 +14,21 @@ export type GrantArray = [BigNumber, string, string, string]; export type GrantEthers = GrantArray & GrantObject; export type Grant = GrantObject | GrantEthers; -// Donation object from GrantRoundManager +// SwapSummary struct from GrantRoundManager +export interface SwapSummary { + amountIn: BigNumberish; + amountOutMin: BigNumberish; + path: BytesLike; // encoded swap path +} + +// Donation struct from GrantRoundManager export interface Donation { grantId: BigNumberish; + token: string; // token address + ratio: BigNumberish; rounds: string[]; - path: string; - deadline: BigNumberish; - amountIn: BigNumberish; - amountOutMinimum: BigNumberish; } + // GrantRound object from GrantRoundManager export type GrantRound = { address: string; @@ -38,6 +44,6 @@ export type GrantRound = { metaPtr: string; minContribution: BigNumberish; hasPaidOut: boolean; - error: string|undefined; + error: string | undefined; }; export type GrantRounds = Array;