-
Notifications
You must be signed in to change notification settings - Fork 4
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
base: dev
Are you sure you want to change the base?
Changes from all commits
fe7b3a4
0754c3f
adcdb93
320ed84
316f035
a02e655
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,185 @@ | ||||||||||
// 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; | ||||||||||
|
||||||||||
/// @notice Struct to store campaign details | ||||||||||
/// @param goal The funding goal of the campaign | ||||||||||
/// @param totalPledged Total amount pledged so far | ||||||||||
/// @param deadline Timestamp when the campaign ends | ||||||||||
/// @param beneficiary Address to receive funds if goal is met | ||||||||||
/// @param finalized Whether the campaign has been finalized | ||||||||||
/// @param tokenAddress Address of ERC20 token, or address(0) for ETH | ||||||||||
struct Campaign { | ||||||||||
uint256 goal; | ||||||||||
uint256 totalPledged; | ||||||||||
uint256 deadline; | ||||||||||
address beneficiary; | ||||||||||
bool finalized; | ||||||||||
address tokenAddress; | ||||||||||
} | ||||||||||
|
||||||||||
/// @notice Mapping of pool IDs to Campaign structs | ||||||||||
mapping(uint256 => Campaign) public campaigns; | ||||||||||
|
||||||||||
/// @notice Mapping of pool IDs to contributor addresses to pledge amounts | ||||||||||
mapping(uint256 => mapping(address => uint256)) public pledges; | ||||||||||
|
||||||||||
/// @notice Event emitted when a new campaign is created | ||||||||||
event CampaignCreated(uint256 indexed poolId, uint256 goal, uint256 deadline, address beneficiary, address tokenAddress); | ||||||||||
|
||||||||||
/// @notice Event emitted when a pledge is made | ||||||||||
event Pledged(uint256 indexed poolId, address indexed contributor, uint256 amount); | ||||||||||
|
||||||||||
/// @notice Event emitted when a campaign reaches its goal | ||||||||||
event GoalReached(uint256 indexed poolId); | ||||||||||
|
||||||||||
/// @notice Event emitted when funds are claimed by the beneficiary | ||||||||||
event FundsClaimed(uint256 indexed poolId, address beneficiary, uint256 amount); | ||||||||||
|
||||||||||
/// @notice Event emitted when funds are refunded to a contributor | ||||||||||
event FundsRefunded(uint256 indexed poolId, address contributor, uint256 amount); | ||||||||||
|
||||||||||
/// @notice Constructor to initialize the AssuranceContract | ||||||||||
/// @param _allo The address of the Allo contract | ||||||||||
constructor(address _allo) BaseStrategy(_allo, "AssuranceContract") {} | ||||||||||
|
||||||||||
/// @notice Initializes a new campaign for a pool | ||||||||||
/// @dev This function is called by Allo when a new pool is created | ||||||||||
/// @param _poolId The ID of the pool | ||||||||||
/// @param _data Encoded initialization parameters (goal, deadline, beneficiary, tokenAddress) | ||||||||||
function initialize(uint256 _poolId, bytes memory _data) external override { | ||||||||||
// Call the initialize function from the BaseStrategy | ||||||||||
__BaseStrategy_init(_poolId); | ||||||||||
|
||||||||||
// Decode the initialization data | ||||||||||
(uint256 goal, uint256 deadline, address beneficiary, address tokenAddress) = abi.decode(_data, (uint256, uint256, address, address)); | ||||||||||
|
||||||||||
// Create and store the new campaign | ||||||||||
campaigns[_poolId] = Campaign(goal, 0, deadline, beneficiary, false, tokenAddress); | ||||||||||
|
||||||||||
// Emit an event for the new campaign | ||||||||||
emit CampaignCreated(_poolId, goal, deadline, beneficiary, tokenAddress); | ||||||||||
} | ||||||||||
|
||||||||||
/// @notice Allows a user to pledge funds to a campaign | ||||||||||
/// @param _poolId The ID of the pool/campaign | ||||||||||
/// @param _amount The amount to pledge (in wei for ETH, or token units for ERC20) | ||||||||||
function pledge(uint256 _poolId, uint256 _amount) external payable { | ||||||||||
Campaign storage campaign = campaigns[_poolId]; | ||||||||||
|
||||||||||
// Ensure the campaign hasn't been finalized | ||||||||||
require(!campaign.finalized, "Campaign already finalized"); | ||||||||||
|
||||||||||
if (campaign.tokenAddress == address(0)) { | ||||||||||
// For ETH pledges | ||||||||||
require(msg.value == _amount, "Incorrect ETH amount"); | ||||||||||
campaign.totalPledged += msg.value; | ||||||||||
pledges[_poolId][msg.sender] += msg.value; | ||||||||||
} else { | ||||||||||
// For ERC20 pledges | ||||||||||
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 an event for the pledge | ||||||||||
emit Pledged(_poolId, msg.sender, _amount); | ||||||||||
|
||||||||||
// Check if the goal has been reached | ||||||||||
if (campaign.totalPledged >= campaign.goal) { | ||||||||||
emit GoalReached(_poolId); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
/// @notice Allows the beneficiary to claim funds if the goal is met | ||||||||||
/// @param _poolId The ID of the pool/campaign | ||||||||||
function claimFunds(uint256 _poolId) external { | ||||||||||
Campaign storage campaign = campaigns[_poolId]; | ||||||||||
|
||||||||||
// Ensure the campaign has ended | ||||||||||
require(block.timestamp >= campaign.deadline, "Campaign not ended"); | ||||||||||
// Ensure the goal was reached | ||||||||||
require(campaign.totalPledged >= campaign.goal, "Goal not reached"); | ||||||||||
// Ensure the campaign hasn't been finalized yet | ||||||||||
require(!campaign.finalized, "Funds already claimed"); | ||||||||||
|
||||||||||
// Mark the campaign as finalized | ||||||||||
campaign.finalized = true; | ||||||||||
uint256 amount = campaign.totalPledged; | ||||||||||
|
||||||||||
if (campaign.tokenAddress == address(0)) { | ||||||||||
// For ETH campaigns | ||||||||||
(bool success, ) = campaign.beneficiary.call{value: amount}(""); | ||||||||||
require(success, "ETH transfer failed"); | ||||||||||
} else { | ||||||||||
// For ERC20 campaigns | ||||||||||
IERC20 token = IERC20(campaign.tokenAddress); | ||||||||||
token.safeTransfer(campaign.beneficiary, amount); | ||||||||||
} | ||||||||||
|
||||||||||
// Emit an event for the claimed funds | ||||||||||
emit FundsClaimed(_poolId, campaign.beneficiary, amount); | ||||||||||
} | ||||||||||
|
||||||||||
/// @notice Allows contributors to get a refund if the goal wasn't met | ||||||||||
/// @param _poolId The ID of the pool/campaign | ||||||||||
function refund(uint256 _poolId) external { | ||||||||||
Campaign storage campaign = campaigns[_poolId]; | ||||||||||
|
||||||||||
// Ensure the campaign has ended | ||||||||||
require(block.timestamp >= campaign.deadline, "Campaign not ended"); | ||||||||||
// Ensure the goal was not reached | ||||||||||
require(campaign.totalPledged < campaign.goal, "Goal was reached"); | ||||||||||
// Ensure the campaign hasn't been finalized | ||||||||||
require(!campaign.finalized, "Campaign already finalized"); | ||||||||||
|
||||||||||
// Get the refund amount for the contributor | ||||||||||
uint256 amount = pledges[_poolId][msg.sender]; | ||||||||||
require(amount > 0, "No funds to refund"); | ||||||||||
|
||||||||||
// Reset the pledge amount for the contributor | ||||||||||
pledges[_poolId][msg.sender] = 0; | ||||||||||
|
||||||||||
if (campaign.tokenAddress == address(0)) { | ||||||||||
// For ETH refunds | ||||||||||
(bool success, ) = msg.sender.call{value: amount}(""); | ||||||||||
require(success, "ETH transfer failed"); | ||||||||||
} else { | ||||||||||
// For ERC20 refunds | ||||||||||
IERC20 token = IERC20(campaign.tokenAddress); | ||||||||||
token.safeTransfer(msg.sender, amount); | ||||||||||
} | ||||||||||
|
||||||||||
// Emit an event for the refund | ||||||||||
emit FundsRefunded(_poolId, msg.sender, amount); | ||||||||||
} | ||||||||||
|
||||||||||
function _allocate(address[] memory, uint256[] memory, bytes memory, address) internal virtual override { | ||||||||||
revert("AssuranceContract: Allocate not implemented"); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need to integrate the payout/refund txns here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? Isnt allocate in this case the |
||||||||||
} | ||||||||||
|
||||||||||
function _distribute(address[] memory, bytes memory, address) internal virtual override { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)... There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||
revert("AssuranceContract: Distribute not implemented"); | ||||||||||
} | ||||||||||
|
||||||||||
function _register(address[] memory, bytes memory, address) internal virtual override returns (address[] memory) { | ||||||||||
revert("AssuranceContract: Register not implemented"); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could make use of the allo-v2.1/contracts/strategies/extensions/register/RecipientsExtension.sol Lines 293 to 296 in c583e0e
|
||||||||||
} | ||||||||||
|
||||||||||
// Function to allow the contract to receive ETH | ||||||||||
receive() external payable override {} | ||||||||||
} |
There was a problem hiding this comment.
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 ininitialize
There was a problem hiding this comment.
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