Skip to content

Commit

Permalink
fix: donate and swap tests now pass with GrantRoundManager update
Browse files Browse the repository at this point in the history
  • Loading branch information
mds1 committed Aug 12, 2021
1 parent 095f1ee commit ac5b1ea
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 38 deletions.
34 changes: 27 additions & 7 deletions contracts/contracts/GrantRoundManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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 ---
Expand Down Expand Up @@ -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");
Expand Down
61 changes: 37 additions & 24 deletions contracts/test/GrantRoundManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]);
});
});
});
20 changes: 13 additions & 7 deletions types/src/grants.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, BigNumberish } from 'ethers';
import { BigNumber, BigNumberish, BytesLike } from 'ethers';
import { TokenInfo } from '@uniswap/token-lists';

// --- Types ---
Expand All @@ -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;
Expand All @@ -38,6 +44,6 @@ export type GrantRound = {
metaPtr: string;
minContribution: BigNumberish;
hasPaidOut: boolean;
error: string|undefined;
error: string | undefined;
};
export type GrantRounds = Array<GrantRound>;

0 comments on commit ac5b1ea

Please sign in to comment.