Skip to content

Commit

Permalink
Adds pledge and withdraw to the gasless functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesduncombe committed Apr 10, 2024
1 parent ab25272 commit 0bb1280
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 17 deletions.
28 changes: 21 additions & 7 deletions contracts/fast/Crowdfund.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity 0.8.10;

import "../lib/LibAddressSet.sol";
import "../interfaces/IERC20.sol";
import "../common/AHasContext.sol";
import "../common/AHasForwarder.sol";
import "../common/AHasMembers.sol";
import "../common/AHasGovernors.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
Expand All @@ -11,7 +13,7 @@ import "@openzeppelin/contracts/utils/math/Math.sol";
* @title The `Crowdfund` FAST contract.
* @notice This contract is used to manage a crowdfunding campaign.
*/
contract Crowdfund {
contract Crowdfund is AHasContext {
using LibAddressSet for LibAddressSet.Data;

/// @notice Happens when a function requires an unmet phase.
Expand Down Expand Up @@ -118,6 +120,18 @@ contract Crowdfund {
creationBlock = block.number;
}

/// AHasContext implementation.

// The trusted forwarder in this instance is the parent FAST's trusted forwarder.
function _isTrustedForwarder(address forwarder) internal view override(AHasContext) returns (bool) {
return AHasForwarder(params.fast).isTrustedForwarder(forwarder);
}

// Override base classes to use the AHasContext implementation.
function _msgSender() internal view override(AHasContext) returns (address) {
return AHasContext._msgSender();
}

/// @dev Given a total and a fee in basis points, returns the fee amount rounded up.
function feeAmount() public view returns (uint256) {
return Math.mulDiv(collected, params.basisPointsFee, 10_000, Math.Rounding.Up);
Expand Down Expand Up @@ -147,18 +161,18 @@ contract Crowdfund {
// Make sure the amount is non-zero.
if (amount == 0) revert InconsistentParameter("amount");
// Make sure that the message sender gave us allowance for at least this amount.
uint256 allowance = params.token.allowance(msg.sender, address(this));
uint256 allowance = params.token.allowance(_msgSender(), address(this));
if (allowance < amount) revert InsufficientFunds(amount - allowance);
// Keep track of the pledger - don't throw if already present.
pledgerSet.add(msg.sender, true);
pledgerSet.add(_msgSender(), true);
// Add the pledged amount to the existing pledge.
pledges[msg.sender] += amount;
pledges[_msgSender()] += amount;
// Update the collected amount.
collected += amount;
// Transfer the tokens to this contract.
if (!params.token.transferFrom(msg.sender, address(this), amount)) revert TokenContractError();
if (!params.token.transferFrom(_msgSender(), address(this), amount)) revert TokenContractError();
// Emit!
emit Pledge(msg.sender, amount);
emit Pledge(_msgSender(), amount);
}

/**
Expand Down Expand Up @@ -291,7 +305,7 @@ contract Crowdfund {
}

modifier onlyFastMember() {
if (!isFastMember(msg.sender)) revert RequiresFastMemberCaller();
if (!isFastMember(_msgSender())) revert RequiresFastMemberCaller();
_;
}
}
18 changes: 16 additions & 2 deletions contracts/fast/Distribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity 0.8.10;
import "../lib/LibAddressSet.sol";
import "../lib/LibPaginate.sol";
import "../interfaces/IERC20.sol";
import "../common/AHasContext.sol";
import "../common/AHasForwarder.sol";
import "../common/AHasMembers.sol";
import "../common/AHasAutomatons.sol";
import "./FastAutomatonsFacet.sol";
Expand All @@ -19,7 +21,7 @@ import "./FastAutomatonsFacet.sol";
* - Withdrawal, during which each beneficiary can withdraw their proceeds.
* - Terminated, during which nothing is possible.
*/
contract Distribution {
contract Distribution is AHasContext {
using LibAddressSet for LibAddressSet.Data;

/// @notice Happens when a function requires an unmet phase.
Expand Down Expand Up @@ -129,6 +131,18 @@ contract Distribution {
creationBlock = block.number;
}

/// AHasContext implementation.

// The trusted forwarder in this instance is the parent FAST's trusted forwarder.
function _isTrustedForwarder(address forwarder) internal view override(AHasContext) returns (bool) {
return AHasForwarder(params.fast).isTrustedForwarder(forwarder);
}

// Override base classes to use the AHasContext implementation.
function _msgSender() internal view override(AHasContext) returns (address) {
return AHasContext._msgSender();
}

function advanceToFeeSetup() public onlyDuring(Phase.Funding) onlyFastCaller {
// Make sure that the current distribution has exactly the required amount locked.
uint256 balance = params.token.balanceOf(address(this));
Expand Down Expand Up @@ -284,7 +298,7 @@ contract Distribution {
// Transfer to the beneficiary all of their ownings.
if (!params.token.transfer(beneficiary, amount)) revert TokenContractError();
// Emit!
emit Withdrawal(msg.sender, beneficiary, amount);
emit Withdrawal(_msgSender(), beneficiary, amount);
}

/**
Expand Down
57 changes: 54 additions & 3 deletions test/fast/Crowdfund.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { BigNumber } from "ethers";
import { FakeContract, smock } from "@defi-wonderland/smock";
import { ethers } from "hardhat";
import { SignerWithAddress } from "hardhat-deploy-ethers/signers";
import { Crowdfund, Crowdfund__factory, IERC20 } from "../../typechain";
import { abiStructToObj, CrowdFundPhase } from "../utils";
import { Crowdfund, Crowdfund__factory, IERC20, IForwarder } from "../../typechain";
import { abiStructToObj, CrowdFundPhase, impersonateContract } from "../utils";
import {
Fast,
Issuer,
Marketplace,
Marketplace
} from "../../typechain/hardhat-diamond-abi/HardhatDiamondABI.sol";
chai.use(solidity);
chai.use(smock.matchers);
Expand All @@ -28,11 +28,18 @@ describe("Crowdfunds", () => {
marketplace: FakeContract<Marketplace>,
fast: FakeContract<Fast>,
erc20: FakeContract<IERC20>,
forwarder: FakeContract<IForwarder>,
crowdfund: Crowdfund,
crowdfundAsIssuer: Crowdfund,
validParams: Crowdfund.ParamsStruct,
deployCrowdfund: (params: Crowdfund.ParamsStruct) => void;

const resetIForwarderMock = () => {
forwarder.supportsInterface.reset();
forwarder.supportsInterface.whenCalledWith(/* IForwarder */ "0x25e23e64").returns(true);
forwarder.supportsInterface.returns(false);
}

before(async () => {
// Keep track of a few signers.
[deployer, issuerMember, governor, alice, bob, paul, ben] =
Expand All @@ -45,6 +52,7 @@ describe("Crowdfunds", () => {
marketplace = await smock.fake("Marketplace");
fast = await smock.fake("Fast");
erc20 = await smock.fake("IERC20");
forwarder = await smock.fake("IForwarder");

issuer.isMember.reset();
issuer.isMember.whenCalledWith(issuerMember.address).returns(true);
Expand Down Expand Up @@ -75,6 +83,8 @@ describe("Crowdfunds", () => {
// Bob and Paul are FAST members.
fast.isMember.whenCalledWith(bob.address).returns(true);
fast.isMember.whenCalledWith(paul.address).returns(true);
// Trusted forwarder setup.
fast.isTrustedForwarder.whenCalledWith(forwarder.address).returns(true);

erc20.balanceOf.reset();
erc20.transfer.reset();
Expand Down Expand Up @@ -105,6 +115,18 @@ describe("Crowdfunds", () => {
.deploy({ ...params, fast: fast.address });
crowdfundAsIssuer = crowdfund.connect(issuerMember);
};

resetIForwarderMock();
});

describe("AHasContext implementation", () => {
describe("_isTrustedForwarder", () => {
it("returns true if the address is a trusted forwarder");
});

describe("_msgSender", () => {
it("returns the original msg.sender");
});
});

describe("various synthesized getters", () => {
Expand Down Expand Up @@ -401,6 +423,35 @@ describe("Crowdfunds", () => {
.to.emit(crowdfund, "Pledge")
.withArgs(alice.address, 500);
});

it("is callable by a trusted forwarder", async () => {
// Impersonate the trusted forwarder contract.
const crowdfundsAsForwarder = await impersonateContract(crowdfund, forwarder.address);

// Set the allowance and transferFrom to succeed.
erc20.allowance.returns(20);
erc20.transferFrom.returns(true);

// Build the data to call the sponsored function.
// Pack the original msg.sender address at the end - this is sponsored callers address.
const encodedFunctionCall = await crowdfund.interface.encodeFunctionData("pledge", [20]);
const data = ethers.utils.solidityPack(
["bytes", "address"],
[encodedFunctionCall, alice.address]
);

// As the forwarder send the packed transaction.
await crowdfundsAsForwarder.signer.sendTransaction(
{
data: data,
to: crowdfund.address,
}
);

// Inspect the owner of the crowdfund.
const [pledgers] = await crowdfund.paginatePledgers(0, 2);
expect(pledgers).to.have.members([alice.address]);
});
});
});

Expand Down
51 changes: 49 additions & 2 deletions test/fast/Distribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import {
Distribution__factory,
IERC20,
Fast,
IForwarder
} from "../../typechain";
import { abiStructToObj, DistributionPhase } from "../utils";
import { abiStructToObj, DistributionPhase, impersonateContract } from "../utils";
import { FastAutomatonPrivilege } from "../../src/utils";
chai.use(solidity);
chai.use(smock.matchers);
Expand All @@ -32,12 +33,19 @@ describe("Distribution", () => {
marketplace: FakeContract<Marketplace>,
fast: FakeContract<Fast>,
erc20: FakeContract<IERC20>,
forwarder: FakeContract<IForwarder>,
distribution: Distribution,
distributionAsIssuer: Distribution,
distributionAsAutomaton: Distribution,
validParams: Distribution.ParamsStruct,
deployDistribution: (params: Distribution.ParamsStruct) => void;

const resetIForwarderMock = () => {
forwarder.supportsInterface.reset();
forwarder.supportsInterface.whenCalledWith(/* IForwarder */ "0x25e23e64").returns(true);
forwarder.supportsInterface.returns(false);
}

before(async () => {
// Keep track of a few signers.
[deployer, issuerMember, governor, automaton, alice, bob, paul, ben] =
Expand All @@ -50,6 +58,7 @@ describe("Distribution", () => {
marketplace = await smock.fake("Marketplace");
fast = await smock.fake("Fast");
erc20 = await smock.fake("IERC20");
forwarder = await smock.fake("IForwarder");

issuer.isMember.reset();
issuer.isMember.whenCalledWith(issuerMember.address).returns(true);
Expand All @@ -73,6 +82,8 @@ describe("Distribution", () => {
fast.isMember.whenCalledWith(bob.address).returns(true);
fast.isMember.whenCalledWith(paul.address).returns(true);
fast.automatonCan.reset();
// Trusted forwarder setup.
fast.isTrustedForwarder.whenCalledWith(forwarder.address).returns(true);

erc20.balanceOf.reset();
erc20.transfer.reset();
Expand Down Expand Up @@ -103,6 +114,18 @@ describe("Distribution", () => {
distributionAsIssuer = distribution.connect(issuerMember);
distributionAsAutomaton = distribution.connect(automaton);
};

resetIForwarderMock();
});

describe("AHasContext implementation", () => {
describe("_isTrustedForwarder", () => {
it("returns true if the address is a trusted forwarder");
});

describe("_msgSender", () => {
it("returns the original msg.sender");
});
});

describe("various synthesized getters", () => {
Expand Down Expand Up @@ -697,7 +720,7 @@ describe("Distribution", () => {
it("marks the withdrawal as done", async () => {
await distribution.withdraw(alice.address);
const subject = await distribution.withdrawn(alice.address);
await expect(subject).to.eq(true);
expect(subject).to.eq(true);
});

it("delegates to ERC20.transfer method", async () => {
Expand All @@ -718,6 +741,30 @@ describe("Distribution", () => {
.to.emit(distribution, "Withdrawal")
.withArgs(deployer.address, alice.address, BigNumber.from(20));
});

it("is callable by a trusted forwarder", async () => {
// Impersonate the trusted forwarder contract.
const crowdfundsAsForwarder = await impersonateContract(distribution, forwarder.address);

// Build the data to call the sponsored function.
// Pack the original msg.sender address at the end - this is sponsored callers address.
const encodedFunctionCall = await distribution.interface.encodeFunctionData("withdraw", [alice.address]);
const data = ethers.utils.solidityPack(
["bytes", "address"],
[encodedFunctionCall, alice.address]
);

// As the forwarder send the packed transaction.
await crowdfundsAsForwarder.signer.sendTransaction(
{
data: data,
to: distribution.address,
}
);

const subject = await distribution.withdrawn(alice.address);
expect(subject).to.eq(true);
});
});
});
});
Expand Down
4 changes: 1 addition & 3 deletions test/marketplace/MarketplaceAccessFacet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ describe("MarketplaceAccessFacet", () => {
});

describe("_msgSender", () => {
it("returns the original msg.sender", async () => {
// Call a function on the Marketplace contract that's sponsored.
});
it("returns the original msg.sender");
});
});

Expand Down

0 comments on commit 0bb1280

Please sign in to comment.