From 3d0d93dd076c529b742c1c18e6f2ec992da46b1c Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Thu, 4 Apr 2024 18:13:10 +0100 Subject: [PATCH] wip --- contracts/fast/Crowdfund.sol | 16 ++++++++++++- contracts/fast/Distribution.sol | 16 ++++++++++++- contracts/fast/FastCrowdfundsFacet.sol | 24 ++++++++++++++----- contracts/fast/FastDistributionsFacet.sol | 26 ++++++++++++++------ test/fast/FastCrowdfundsFacet.test.ts | 28 +++++++++++++++++++++- test/fast/FastDistributionsFacet.test.ts | 29 ++++++++++++++++++++++- 6 files changed, 122 insertions(+), 17 deletions(-) diff --git a/contracts/fast/Crowdfund.sol b/contracts/fast/Crowdfund.sol index 0ecf0dac..93483946 100644 --- a/contracts/fast/Crowdfund.sol +++ b/contracts/fast/Crowdfund.sol @@ -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"; @@ -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. @@ -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); diff --git a/contracts/fast/Distribution.sol b/contracts/fast/Distribution.sol index b3dc512b..91713839 100644 --- a/contracts/fast/Distribution.sol +++ b/contracts/fast/Distribution.sol @@ -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"; @@ -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. @@ -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)); diff --git a/contracts/fast/FastCrowdfundsFacet.sol b/contracts/fast/FastCrowdfundsFacet.sol index 437220a7..a1aed7aa 100644 --- a/contracts/fast/FastCrowdfundsFacet.sol +++ b/contracts/fast/FastCrowdfundsFacet.sol @@ -1,34 +1,46 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.10; -import "./lib/AFastFacet.sol"; -import "./FastTopFacet.sol"; import "../lib/LibPaginate.sol"; +import "../common/AHasContext.sol"; +import "../interfaces/ICustomErrors.sol"; import "../issuer/IssuerAutomatonsFacet.sol"; +import "./lib/AFastFacet.sol"; import "./lib/LibFastCrowdfunds.sol"; +import "./FastTopFacet.sol"; import "./Crowdfund.sol"; -import "../interfaces/ICustomErrors.sol"; /** * @title The Fast Smart Contract. * @notice The Fast Crowdfunds facet is in charge of deploying and keeping track of crowdfunds. */ -contract FastCrowdfundsFacet is AFastFacet { +contract FastCrowdfundsFacet is AFastFacet, AHasContext { using LibAddressSet for LibAddressSet.Data; /// @notice Happens when there are insufficient funds somewhere. error RequiresPrivilege(address who, uint32 privilege); + /// AHasContext implementation. + + function _isTrustedForwarder(address forwarder) internal view override(AHasContext) returns (bool) { + return AHasForwarder(address(this)).isTrustedForwarder(forwarder); + } + + // Override base classes to use the AHasContext implementation. + function _msgSender() internal view override(AHasContext) returns (address) { + return AHasContext._msgSender(); + } + /** * @notice Creates a crowdfund contract. * @param token is the address of the ERC20 token that should be collected. */ - function createCrowdfund(IERC20 token, address beneficiary, string memory ref) external onlyGovernor(msg.sender) { + function createCrowdfund(IERC20 token, address beneficiary, string memory ref) external onlyGovernor(_msgSender()) { address issuer = FastTopFacet(address(this)).issuerAddress(); // Deploy a new Crowdfund contract. Crowdfund crowdfund = new Crowdfund( Crowdfund.Params({ - owner: msg.sender, + owner: _msgSender(), issuer: issuer, fast: address(this), beneficiary: beneficiary, diff --git a/contracts/fast/FastDistributionsFacet.sol b/contracts/fast/FastDistributionsFacet.sol index 80f00724..5bff49b6 100644 --- a/contracts/fast/FastDistributionsFacet.sol +++ b/contracts/fast/FastDistributionsFacet.sol @@ -1,17 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.10; -import "./lib/AFastFacet.sol"; -import "./FastTopFacet.sol"; import "../lib/LibPaginate.sol"; +import "../common/AHasContext.sol"; +import "./lib/AFastFacet.sol"; import "./lib/LibFastDistributions.sol"; +import "./FastTopFacet.sol"; import "./Distribution.sol"; /** * @title The Fast Smart Contract. * @notice The Fast Distributions facet is in charge of deploying and keeping track of distributions. */ -contract FastDistributionsFacet is AFastFacet { +contract FastDistributionsFacet is AFastFacet, AHasContext { using LibAddressSet for LibAddressSet.Data; /// @notice Happens when a call to the ERC20 token contract fails. @@ -19,6 +20,17 @@ contract FastDistributionsFacet is AFastFacet { /// @notice Happens when there are insufficient funds somewhere. error InsufficientFunds(uint256 amount); + /// AHasContext implementation. + + function _isTrustedForwarder(address forwarder) internal view override(AHasContext) returns (bool) { + return AHasForwarder(address(this)).isTrustedForwarder(forwarder); + } + + // Override base classes to use the AHasContext implementation. + function _msgSender() internal view override(AHasContext) returns (address) { + return AHasContext._msgSender(); + } + /** * @notice Creates a distribution contract. * @param token is the address of the ERC20 token that should be distributed. @@ -30,9 +42,9 @@ contract FastDistributionsFacet is AFastFacet { uint256 total, uint256 blockLatch, string memory ref - ) external onlyMember(msg.sender) { + ) external onlyMember(_msgSender()) { // Make sure the current FAST contract has at least `total` allowance over the user's ERC20 tokens. - uint256 allowance = token.allowance(msg.sender, address(this)); + uint256 allowance = token.allowance(_msgSender(), address(this)); if (allowance < total) revert InsufficientFunds(total - allowance); // Deploy a new Distribution contract locked onto the current FAST and target currency token. @@ -41,7 +53,7 @@ contract FastDistributionsFacet is AFastFacet { issuer: FastTopFacet(address(this)).issuerAddress(), fast: address(this), blockLatch: blockLatch, - distributor: msg.sender, + distributor: _msgSender(), token: token, total: total, ref: ref @@ -50,7 +62,7 @@ contract FastDistributionsFacet is AFastFacet { // Register our newly created distribution and keep track of it. LibFastDistributions.data().distributionSet.add(address(dist), false); // Transfer the ERC20 tokens to the distribution contract. - if (!token.transferFrom(msg.sender, address(dist), total)) revert TokenContractError(); + if (!token.transferFrom(_msgSender(), address(dist), total)) revert TokenContractError(); // Advance to the FeeSetup phase - only the FAST contract can do that. dist.advanceToFeeSetup(); // Emit! diff --git a/test/fast/FastCrowdfundsFacet.test.ts b/test/fast/FastCrowdfundsFacet.test.ts index e62dcf0a..e685bd22 100644 --- a/test/fast/FastCrowdfundsFacet.test.ts +++ b/test/fast/FastCrowdfundsFacet.test.ts @@ -10,6 +10,8 @@ import { IERC20, Crowdfund, FastFrontendFacet, + IForwarder, + Fast } from "../../typechain"; import { fastFixtureFunc } from "../fixtures/fast"; import { @@ -32,6 +34,8 @@ describe("FastCrowdfundsFacet", () => { marketplace: FakeContract, erc20: FakeContract, frontendMock: FakeContract, + forwarder: FakeContract, + fast: Fast, crowdfunds: FastCrowdfundsFacet, crowdfundsAsMember: FastCrowdfundsFacet, crowdfundsAsGovernor: FastCrowdfundsFacet, @@ -47,6 +51,7 @@ describe("FastCrowdfundsFacet", () => { issuer = await smock.fake("Issuer"); marketplace = await smock.fake("Marketplace"); erc20 = await smock.fake("IERC20"); + forwarder = await smock.fake("IForwarder"); marketplace.issuerAddress.returns(issuer.address); }); @@ -84,7 +89,7 @@ describe("FastCrowdfundsFacet", () => { name: "FastCrowdfundsFixture", deployer: deployer.address, afterDeploy: async (args) => { - const { fast } = args; + ({ fast } = args); ({ frontendMock } = args); crowdfunds = await ethers.getContractAt( "FastCrowdfundsFacet", @@ -108,6 +113,27 @@ describe("FastCrowdfundsFacet", () => { }); }); + describe("AHasContext implementation", () => { + describe("_isTrustedForwarder", () => { + it("returns true if the address is a trusted forwarder", async () => { + // Set the trusted forwarder. + forwarder.supportsInterface.reset(); + forwarder.supportsInterface.whenCalledWith(/* IForwarder */ "0x25e23e64").returns(true); + forwarder.supportsInterface.returns(false); + + await fast.connect(issuerMember).setTrustedForwarder(forwarder.address); + + // isTrustedForwarder() should delegate to _isTrustedForwarder(). + const subject = await fast.connect(issuerMember).isTrustedForwarder(forwarder.address); + expect(subject).to.eq(true); + }); + }); + + describe("_msgSender", () => { + it("returns the original msg.sender"); + }); + }); + describe("crowdfundsDefaultBasisPointFee", () => { it("returns the default basis point fee", async () => { const subject = await crowdfunds.crowdfundsDefaultBasisPointFee(); diff --git a/test/fast/FastDistributionsFacet.test.ts b/test/fast/FastDistributionsFacet.test.ts index cc69f57c..3eef3470 100644 --- a/test/fast/FastDistributionsFacet.test.ts +++ b/test/fast/FastDistributionsFacet.test.ts @@ -9,6 +9,8 @@ import { FastDistributionsFacet, IERC20, Distribution, + IForwarder, + Fast } from "../../typechain"; import { fastFixtureFunc } from "../fixtures/fast"; import { DistributionPhase } from "../utils"; @@ -30,6 +32,8 @@ describe("FastDistributionsFacet", () => { let issuer: FakeContract, marketplace: FakeContract, erc20: FakeContract, + forwarder: FakeContract, + fast: Fast, distributions: FastDistributionsFacet, distributionsAsMember: FastDistributionsFacet; @@ -43,6 +47,7 @@ describe("FastDistributionsFacet", () => { issuer = await smock.fake("Issuer"); marketplace = await smock.fake("Marketplace"); erc20 = await smock.fake("IERC20"); + forwarder = await smock.fake("IForwarder"); marketplace.issuerAddress.returns(issuer.address); }); @@ -76,7 +81,8 @@ describe("FastDistributionsFacet", () => { opts: { name: "FastDistributionsFixture", deployer: deployer.address, - afterDeploy: async ({ fast }) => { + afterDeploy: async (args) => { + ({ fast } = args); await fast.connect(issuerMember).addGovernor(governor.address); distributions = await ethers.getContractAt( "FastDistributionsFacet", @@ -97,6 +103,27 @@ describe("FastDistributionsFacet", () => { }); }); + describe("AHasContext implementation", () => { + describe("_isTrustedForwarder", () => { + it("returns true if the address is a trusted forwarder", async () => { + // Set the trusted forwarder. + forwarder.supportsInterface.reset(); + forwarder.supportsInterface.whenCalledWith(/* IForwarder */ "0x25e23e64").returns(true); + forwarder.supportsInterface.returns(false); + + await fast.connect(issuerMember).setTrustedForwarder(forwarder.address); + + // isTrustedForwarder() should delegate to _isTrustedForwarder(). + const subject = await fast.connect(issuerMember).isTrustedForwarder(forwarder.address); + expect(subject).to.eq(true); + }); + }); + + describe("_msgSender", () => { + it("returns the original msg.sender"); + }); + }); + /// Governorship related stuff. describe("createDistribution", () => {