Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MVP of assurance contract contracts + tests #44

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions contracts/strategies/examples/assurance-contract/AssuranceContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;

import {BaseStrategy} from "../../BaseStrategy.sol";
import {IAllo} from "../../../core/interfaces/IAllo.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title AssuranceContract
/// @notice This contract implements an assurance contract strategy for crowdfunding, supporting both ETH and ERC20 tokens.
/// @dev Extends BaseStrategy to integrate with the Allo protocol.
contract AssuranceContract is BaseStrategy {
using SafeERC20 for IERC20;

struct Campaign {
uint256 goal;
uint256 totalPledged;
uint256 deadline;
address beneficiary;
bool finalized;
address tokenAddress; // Address of ERC20 token, or address(0) for ETH
}

mapping(uint256 => Campaign) public campaigns;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you have this mapping here, does this mean the strategy would be re-usable for multiple campaigns after each one finishes? If so it would be cleaner to also include a createCampaign function for pool managers that will basically create the campaign like you do in initialize

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe pool ID here is also reffering to the ID of the campaign, this might confuse people with Allo's pool IDs

mapping(uint256 => mapping(address => uint256)) public pledges;

event CampaignCreated(uint256 indexed poolId, uint256 goal, uint256 deadline, address beneficiary, address tokenAddress);
event Pledged(uint256 indexed poolId, address indexed contributor, uint256 amount);
event GoalReached(uint256 indexed poolId);
event FundsClaimed(uint256 indexed poolId, address beneficiary, uint256 amount);
event FundsRefunded(uint256 indexed poolId, address contributor, uint256 amount);

constructor(address _allo) BaseStrategy(_allo, "AssuranceContract") {}

function initialize(uint256 _poolId, bytes memory _data) external override {
__BaseStrategy_init(_poolId);

(uint256 goal, uint256 deadline, address beneficiary, address tokenAddress) = abi.decode(_data, (uint256, uint256, address, address));

campaigns[_poolId] = Campaign(goal, 0, deadline, beneficiary, false, tokenAddress);

emit CampaignCreated(_poolId, goal, deadline, beneficiary, tokenAddress);
}

function pledge(uint256 _poolId, uint256 _amount) external payable {
Campaign storage campaign = campaigns[_poolId];

require(block.timestamp < campaign.deadline, "Campaign ended");
require(!campaign.finalized, "Campaign already finalized");

if (campaign.tokenAddress == address(0)) {
require(msg.value == _amount, "Incorrect ETH amount");
campaign.totalPledged += msg.value;
pledges[_poolId][msg.sender] += msg.value;
} else {
IERC20 token = IERC20(campaign.tokenAddress);
uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), _amount);
uint256 balanceAfter = token.balanceOf(address(this));
uint256 actualAmount = balanceAfter - balanceBefore;
campaign.totalPledged += actualAmount;
pledges[_poolId][msg.sender] += actualAmount;
}

emit Pledged(_poolId, msg.sender, _amount);

if (campaign.totalPledged >= campaign.goal) {
emit GoalReached(_poolId);
}
}

function claimFunds(uint256 _poolId) external {
Campaign storage campaign = campaigns[_poolId];

require(block.timestamp >= campaign.deadline, "Campaign not ended");
require(campaign.totalPledged >= campaign.goal, "Goal not reached");
require(!campaign.finalized, "Funds already claimed");

campaign.finalized = true;
uint256 amount = campaign.totalPledged;

if (campaign.tokenAddress == address(0)) {
(bool success, ) = campaign.beneficiary.call{value: amount}("");
require(success, "ETH transfer failed");
} else {
IERC20 token = IERC20(campaign.tokenAddress);
token.safeTransfer(campaign.beneficiary, amount);
}

emit FundsClaimed(_poolId, campaign.beneficiary, amount);
}

function refund(uint256 _poolId) external {
Campaign storage campaign = campaigns[_poolId];

require(block.timestamp >= campaign.deadline, "Campaign not ended");
require(campaign.totalPledged < campaign.goal, "Goal was reached");
require(!campaign.finalized, "Campaign already finalized");

uint256 amount = pledges[_poolId][msg.sender];
require(amount > 0, "No funds to refund");

pledges[_poolId][msg.sender] = 0;

if (campaign.tokenAddress == address(0)) {
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "ETH transfer failed");
} else {
IERC20 token = IERC20(campaign.tokenAddress);
token.safeTransfer(msg.sender, amount);
}

emit FundsRefunded(_poolId, msg.sender, amount);
}

function _allocate(address[] memory, uint256[] memory, bytes memory, address) internal virtual override {
revert("AssuranceContract: Allocate not implemented");
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to integrate the payout/refund txns here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? Isnt allocate in this case the pledge logic?

}

function _distribute(address[] memory, bytes memory, address) internal virtual override {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice if i just got a distribute function that could natively handle any type of distribution (merkle, drips, or otherwise)...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. In this example however what would distribute do? like allocate - pledge, I think distribute should hold the logic of claimFunds

revert("AssuranceContract: Distribute not implemented");
}

function _register(address[] memory, bytes memory, address) internal virtual override returns (address[] memory) {
revert("AssuranceContract: Register not implemented");
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the current allo pool uses just naked addresses. need to use allo registry items per https://github.com/allo-protocol/allo-v2.1/blob/dev/contracts/core/Allo.md#register-recipient.

would be nice if this could be inherited from an upstream contract..

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could make use of the RecipientsExtension. Using the _register from the extensions includes this check

if (_recipientIdOrRegistryAnchor != address(0)) {
if (!_isProfileMember(_recipientIdOrRegistryAnchor, _sender)) {
revert UNAUTHORIZED();
}

}

// Function to allow the contract to receive ETH
receive() external payable override {}
}
230 changes: 230 additions & 0 deletions test/integration/AssuranceContractStrategy.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import {AssuranceContract} from "../../contracts/strategies/examples/assurance-contract/AssuranceContract.sol";
import {BaseStrategy} from "../../contracts/strategies/BaseStrategy.sol";
import {Metadata} from "../../contracts/core/libraries/Metadata.sol";
import {IAllo} from "../../contracts/core/interfaces/IAllo.sol";
import {MockERC20} from "../mocks/MockERC20.sol";
import {IBaseStrategy} from "../../contracts/strategies/IBaseStrategy.sol";

contract AssuranceContractStrategyTest is Test {
AssuranceContract public assuranceContract;
MockERC20 public mockToken;

address public beneficiary;
address public contributor1;
address public contributor2;
address public mockAllo;

uint256 public poolId;
uint256 public goal;
uint256 public deadline;

event CampaignCreated(uint256 indexed poolId, uint256 goal, uint256 deadline, address beneficiary, address tokenAddress);
event Pledged(uint256 indexed poolId, address indexed contributor, uint256 amount);
event GoalReached(uint256 indexed poolId);
event FundsClaimed(uint256 indexed poolId, address beneficiary, uint256 amount);
event FundsRefunded(uint256 indexed poolId, address contributor, uint256 amount);

function setUp() public {
mockAllo = address(this);
assuranceContract = new AssuranceContract(mockAllo);
mockToken = new MockERC20("Mock Token", "MTK", 18);

beneficiary = makeAddr("beneficiary");
contributor1 = makeAddr("contributor1");
contributor2 = makeAddr("contributor2");

poolId = 1;
goal = 10 ether;
deadline = block.timestamp + 1 days;
}

function testInitializeETH() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(0));
vm.expectEmit(true, true, true, true);
emit CampaignCreated(poolId, goal, deadline, beneficiary, address(0));
assuranceContract.initialize(poolId, initData);

(uint256 _goal, uint256 _totalPledged, uint256 _deadline, address _beneficiary, bool _finalized, address _tokenAddress) = assuranceContract.campaigns(poolId);
assertEq(_goal, goal);
assertEq(_totalPledged, 0);
assertEq(_deadline, deadline);
assertEq(_beneficiary, beneficiary);
assertFalse(_finalized);
assertEq(_tokenAddress, address(0));
}

function testInitializeERC20() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(mockToken));
vm.expectEmit(true, true, true, true);
emit CampaignCreated(poolId, goal, deadline, beneficiary, address(mockToken));
assuranceContract.initialize(poolId, initData);

(uint256 _goal, uint256 _totalPledged, uint256 _deadline, address _beneficiary, bool _finalized, address _tokenAddress) = assuranceContract.campaigns(poolId);
assertEq(_goal, goal);
assertEq(_totalPledged, 0);
assertEq(_deadline, deadline);
assertEq(_beneficiary, beneficiary);
assertFalse(_finalized);
assertEq(_tokenAddress, address(mockToken));
}

function testPledgeETH() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(0));
assuranceContract.initialize(poolId, initData);

uint256 pledgeAmount = 1 ether;
vm.deal(contributor1, pledgeAmount);
vm.prank(contributor1);
vm.expectEmit(true, true, true, true);
emit Pledged(poolId, contributor1, pledgeAmount);
assuranceContract.pledge{value: pledgeAmount}(poolId, pledgeAmount);

assertEq(assuranceContract.pledges(poolId, contributor1), pledgeAmount);
}

function testPledgeERC20() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(mockToken));
assuranceContract.initialize(poolId, initData);

uint256 pledgeAmount = 1 ether;
mockToken.mint(contributor1, pledgeAmount);
vm.startPrank(contributor1);
mockToken.approve(address(assuranceContract), pledgeAmount);
vm.expectEmit(true, true, true, true);
emit Pledged(poolId, contributor1, pledgeAmount);
assuranceContract.pledge(poolId, pledgeAmount);
vm.stopPrank();

assertEq(assuranceContract.pledges(poolId, contributor1), pledgeAmount);
}

function testClaimFundsETH() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(0));
assuranceContract.initialize(poolId, initData);

vm.deal(contributor1, goal);
vm.prank(contributor1);
assuranceContract.pledge{value: goal}(poolId, goal);

vm.warp(deadline + 1);
vm.prank(beneficiary);
vm.expectEmit(true, true, true, true);
emit FundsClaimed(poolId, beneficiary, goal);
assuranceContract.claimFunds(poolId);

assertEq(beneficiary.balance, goal);
}

function testClaimFundsERC20() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(mockToken));
assuranceContract.initialize(poolId, initData);

mockToken.mint(contributor1, goal);
vm.startPrank(contributor1);
mockToken.approve(address(assuranceContract), goal);
assuranceContract.pledge(poolId, goal);
vm.stopPrank();

vm.warp(deadline + 1);
vm.prank(beneficiary);
vm.expectEmit(true, true, true, true);
emit FundsClaimed(poolId, beneficiary, goal);
assuranceContract.claimFunds(poolId);

assertEq(mockToken.balanceOf(beneficiary), goal);
}

function testRefundETH() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(0));
assuranceContract.initialize(poolId, initData);

uint256 pledgeAmount = 1 ether;
vm.deal(contributor1, pledgeAmount);
vm.prank(contributor1);
assuranceContract.pledge{value: pledgeAmount}(poolId, pledgeAmount);

vm.warp(deadline + 1);
vm.prank(contributor1);
vm.expectEmit(true, true, true, true);
emit FundsRefunded(poolId, contributor1, pledgeAmount);
assuranceContract.refund(poolId);

assertEq(contributor1.balance, pledgeAmount);
assertEq(assuranceContract.pledges(poolId, contributor1), 0);
}

function testRefundERC20() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(mockToken));
assuranceContract.initialize(poolId, initData);

uint256 pledgeAmount = 1 ether;
mockToken.mint(contributor1, pledgeAmount);
vm.startPrank(contributor1);
mockToken.approve(address(assuranceContract), pledgeAmount);
assuranceContract.pledge(poolId, pledgeAmount);
vm.stopPrank();

vm.warp(deadline + 1);
vm.prank(contributor1);
vm.expectEmit(true, true, true, true);
emit FundsRefunded(poolId, contributor1, pledgeAmount);
assuranceContract.refund(poolId);

assertEq(mockToken.balanceOf(contributor1), pledgeAmount);
assertEq(assuranceContract.pledges(poolId, contributor1), 0);
}

function testPledgeAfterDeadline() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(0));
assuranceContract.initialize(poolId, initData);

vm.warp(deadline + 1);
vm.deal(contributor1, 1 ether);
vm.prank(contributor1);
vm.expectRevert("Campaign ended");
assuranceContract.pledge{value: 1 ether}(poolId, 1 ether);
}

function testClaimFundsBeforeDeadline() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(0));
assuranceContract.initialize(poolId, initData);

vm.deal(contributor1, goal);
vm.prank(contributor1);
assuranceContract.pledge{value: goal}(poolId, goal);

vm.prank(beneficiary);
vm.expectRevert("Campaign not ended");
assuranceContract.claimFunds(poolId);
}

function testRefundBeforeDeadline() public {
bytes memory initData = abi.encode(goal, deadline, beneficiary, address(0));
assuranceContract.initialize(poolId, initData);

uint256 pledgeAmount = 1 ether;
vm.deal(contributor1, pledgeAmount);
vm.prank(contributor1);
assuranceContract.pledge{value: pledgeAmount}(poolId, pledgeAmount);

vm.prank(contributor1);
vm.expectRevert("Campaign not ended");
assuranceContract.refund(poolId);
}

// Mock functions to satisfy IAllo interface
function getPool(uint256) external view returns (IAllo.Pool memory) {
return IAllo.Pool({
profileId: bytes32(0),
strategy: IBaseStrategy(address(0)),
token: address(0),
metadata: Metadata(0, ""),
managerRole: bytes32(0),
adminRole: bytes32(0)
});
}
}
Loading