-
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 3 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,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; | ||||||||||
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"); | ||||||||||
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 {} | ||||||||||
} |
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) | ||
}); | ||
} | ||
} |
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