diff --git a/packages/contracts/contracts/test/MockSale.sol b/packages/contracts/contracts/test/MockSale.sol index 89b23630..3afe3bd3 100644 --- a/packages/contracts/contracts/test/MockSale.sol +++ b/packages/contracts/contracts/test/MockSale.sol @@ -16,6 +16,7 @@ contract MockSale is ISale, ERC165 { mapping(address => uint256) public override(ISale) refundAmount; mapping(string => uint256) public calls; + address public token; address public paymentToken; function paymentTokenToToken( diff --git a/packages/contracts/contracts/token/ISale.sol b/packages/contracts/contracts/token/ISale.sol index 8b21c9e9..e297677f 100644 --- a/packages/contracts/contracts/token/ISale.sol +++ b/packages/contracts/contracts/token/ISale.sol @@ -2,7 +2,10 @@ pragma solidity ^0.8.20; interface ISale { - /// The $aUSD token + /// The $CTND token + function token() external view returns (address); + + /// The $USDC token function paymentToken() external view returns (address); /// How many $CTND will be received for the given payment amount diff --git a/packages/contracts/contracts/token/Sale.sol b/packages/contracts/contracts/token/Sale.sol index 722d80e9..96c424e1 100644 --- a/packages/contracts/contracts/token/Sale.sol +++ b/packages/contracts/contracts/token/Sale.sol @@ -46,6 +46,9 @@ contract Sale is ISale, RisingTide, ERC165, AccessControl, ReentrancyGuard { uint256 tokenAmount ); + /// Emitted for every claim + event Claim(address indexed to, uint256 tokenAmount); + /// Emitted for every refund given event Refund(address indexed to, uint256 paymentTokenAmount); @@ -56,6 +59,9 @@ contract Sale is ISale, RisingTide, ERC165, AccessControl, ReentrancyGuard { // State // + /// See {ISale.token} + address public override(ISale) token; + /// See {ISale.paymentToken} address public immutable override(ISale) paymentToken; @@ -153,6 +159,11 @@ contract Sale is ISale, RisingTide, ERC165, AccessControl, ReentrancyGuard { _grantRole(CAP_VALIDATOR_ROLE, msg.sender); } + modifier beforeSale() { + require(block.timestamp <= start, "sale active"); + _; + } + /// Ensures we're running during the set sale period modifier inSale() { require( @@ -240,10 +251,25 @@ contract Sale is ISale, RisingTide, ERC165, AccessControl, ReentrancyGuard { ); } + function claim() external capCalculated nonReentrant { + Account storage account = accounts[msg.sender]; + require(!account.refunded, "already claimed"); + + if (refundAmount(msg.sender) != 0) { + refund(msg.sender); + } + + uint256 capped = allocation(msg.sender); + + IERC20(token).transfer(msg.sender, capped); + + emit Claim(msg.sender, capped); + } + /// @inheritdoc ISale function refund( address to - ) external override(ISale) capCalculated nonReentrant { + ) public override(ISale) capCalculated nonReentrant { Account storage account = accounts[to]; require(!account.refunded, "already refunded"); @@ -336,6 +362,12 @@ contract Sale is ISale, RisingTide, ERC165, AccessControl, ReentrancyGuard { // Admin API // + function setToken( + address _token + ) external onlyRole(DEFAULT_ADMIN_ROLE) beforeSale nonReentrant { + token = _token; + } + /// Sets the individual cap /// @dev Can only be called once /// diff --git a/packages/contracts/script/Deploy.s.sol b/packages/contracts/script/Deploy.s.sol index bb9c57ef..79342370 100644 --- a/packages/contracts/script/Deploy.s.sol +++ b/packages/contracts/script/Deploy.s.sol @@ -12,8 +12,8 @@ contract DeployScript is Script { uint256 start = vm.unixTime(); uint256 end = start + 60 * 60 * 24 * 7; - uint256 startRegistration = 1714089600; - uint256 endRegistration = 1714694400; + uint256 startRegistration = 1714089600000; + uint256 endRegistration = 1714694400000; MockERC20 token = new MockERC20("USDC", "USDC", 18); Sale sale = new Sale( diff --git a/packages/contracts/script/DevDeploy.s.sol b/packages/contracts/script/DevDeploy.s.sol index 576feea2..0f18dea5 100644 --- a/packages/contracts/script/DevDeploy.s.sol +++ b/packages/contracts/script/DevDeploy.s.sol @@ -36,8 +36,8 @@ contract DevDeployScript is Script { bytes32 merkleRoot = 0xa5c09e2a9128afef7246a5900cfe02c4bd2cfcac8ac4286f0159a699c8455a49; - startRegistration = 1714089600; - endRegistration = 1714694400; + startRegistration = 1714089600000; + endRegistration = 1714694400000; start = vm.getBlockTimestamp(); end = start + 60 * 60 * 24; @@ -49,13 +49,15 @@ contract DevDeployScript is Script { 1 ** 18, start, end, - 1000, + 100000000000000000000, 1000000, 2000000, startRegistration, endRegistration ); + bool sucesss = citizend.transfer(address(sale), 1000 ether); + bool success = token.approve(address(sale), 1000 ether); require(success, "approve failed"); diff --git a/packages/contracts/test/contracts/token/Sale.d.sol b/packages/contracts/test/contracts/token/Sale.d.sol index 4ce95fa3..c0327539 100644 --- a/packages/contracts/test/contracts/token/Sale.d.sol +++ b/packages/contracts/test/contracts/token/Sale.d.sol @@ -3,10 +3,12 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import {Sale} from "../../../contracts/token/Sale.sol"; +import {Citizend} from "../../../contracts/token/Citizend.sol"; import {MockERC20} from "../../../contracts/test/MockERC20.sol"; contract SaleTest is Test { Sale sale; + Citizend token; MockERC20 paymentToken; uint256 start; uint256 end; @@ -23,6 +25,8 @@ contract SaleTest is Test { uint256 tokenAmount ); + event Claim(address indexed to, uint256 tokenAmount); + function setUp() public { vm.startPrank(owner); @@ -33,20 +37,24 @@ contract SaleTest is Test { end = start + 60 * 60 * 24; paymentToken = new MockERC20("USDC", "USDC", 18); + token = new Citizend(owner); sale = new Sale( address(paymentToken), 1 ** 18, start, end, - 100, + 1000000000000000000000000, 1000000, 2000000, startRegistration, endRegistration ); + sale.setToken(address(token)); sale.setMaxContribution(4 ether); + token.transfer(address(sale), 1000000 ether); + vm.stopPrank(); vm.startPrank(alice); @@ -111,4 +119,29 @@ contract SaleTest is Test { vm.stopPrank(); } + + function testBuyAndClaim() public { + require(token.balanceOf(alice) == 0 ether); + + vm.prank(alice); + sale.buy(2 ether); + + vm.warp(end + 1000); + + require(sale.uncappedAllocation(address(alice)) == 2 ether); + + vm.prank(owner); + sale.setIndividualCap(2 ether); + + require(sale.risingTide_isValidCap(), "not valid cap"); + require(sale.allocation(address(alice)) == 2 ether); + + vm.expectEmit(); + emit Claim(address(alice), 2 ether); + + vm.prank(alice); + sale.claim(); + + require(token.balanceOf(alice) == 2 ether); + } } diff --git a/packages/web-app/wagmi.generated.ts b/packages/web-app/wagmi.generated.ts index 3d3ade2c..62099fbc 100644 --- a/packages/web-app/wagmi.generated.ts +++ b/packages/web-app/wagmi.generated.ts @@ -3995,6 +3995,13 @@ export const iSaleAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [], + name: 'token', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, { type: 'function', inputs: [ @@ -4553,6 +4560,13 @@ export const mockSaleAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [], + name: 'token', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, { type: 'function', inputs: [{ name: '_x', internalType: 'uint256', type: 'uint256' }], @@ -5939,6 +5953,13 @@ export const saleAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [], + name: 'claim', + outputs: [], + stateMutability: 'nonpayable', + }, { type: 'function', inputs: [], @@ -6178,6 +6199,13 @@ export const saleAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [{ name: '_token', internalType: 'address', type: 'address' }], + name: 'setToken', + outputs: [], + stateMutability: 'nonpayable', + }, { type: 'function', inputs: [], @@ -6199,6 +6227,13 @@ export const saleAbi = [ outputs: [{ name: '', internalType: 'bool', type: 'bool' }], stateMutability: 'view', }, + { + type: 'function', + inputs: [], + name: 'token', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, { type: 'function', inputs: [ @@ -6243,6 +6278,20 @@ export const saleAbi = [ outputs: [{ name: '', internalType: 'bool', type: 'bool' }], stateMutability: 'view', }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'to', internalType: 'address', type: 'address', indexed: true }, + { + name: 'tokenAmount', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'Claim', + }, { type: 'event', anonymous: false, @@ -6548,6 +6597,13 @@ export const saleTestAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [], + name: 'testBuyAndClaim', + outputs: [], + stateMutability: 'nonpayable', + }, { type: 'function', inputs: [], @@ -6562,6 +6618,20 @@ export const saleTestAbi = [ outputs: [], stateMutability: 'nonpayable', }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'to', internalType: 'address', type: 'address', indexed: true }, + { + name: 'tokenAmount', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'Claim', + }, { type: 'event', anonymous: false, @@ -17398,6 +17468,14 @@ export const useReadISaleRefundAmount = /*#__PURE__*/ createUseReadContract({ functionName: 'refundAmount', }) +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link iSaleAbi}__ and `functionName` set to `"token"` + */ +export const useReadISaleToken = /*#__PURE__*/ createUseReadContract({ + abi: iSaleAbi, + functionName: 'token', +}) + /** * Wraps __{@link useReadContract}__ with `abi` set to __{@link iSaleAbi}__ and `functionName` set to `"tokenToPaymentToken"` */ @@ -18208,6 +18286,14 @@ export const useReadMockSaleTestCalled = /*#__PURE__*/ createUseReadContract({ functionName: 'test_Called', }) +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link mockSaleAbi}__ and `functionName` set to `"token"` + */ +export const useReadMockSaleToken = /*#__PURE__*/ createUseReadContract({ + abi: mockSaleAbi, + functionName: 'token', +}) + /** * Wraps __{@link useReadContract}__ with `abi` set to __{@link mockSaleAbi}__ and `functionName` set to `"tokenToPaymentToken"` */ @@ -20021,6 +20107,14 @@ export const useReadSaleSupportsInterface = /*#__PURE__*/ createUseReadContract( { abi: saleAbi, functionName: 'supportsInterface' }, ) +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"token"` + */ +export const useReadSaleToken = /*#__PURE__*/ createUseReadContract({ + abi: saleAbi, + functionName: 'token', +}) + /** * Wraps __{@link useReadContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"tokenToPaymentToken"` */ @@ -20080,6 +20174,14 @@ export const useWriteSaleBuy = /*#__PURE__*/ createUseWriteContract({ functionName: 'buy', }) +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"claim"` + */ +export const useWriteSaleClaim = /*#__PURE__*/ createUseWriteContract({ + abi: saleAbi, + functionName: 'claim', +}) + /** * Wraps __{@link useWriteContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"grantRole"` */ @@ -20148,6 +20250,14 @@ export const useWriteSaleSetMinContribution = functionName: 'setMinContribution', }) +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"setToken"` + */ +export const useWriteSaleSetToken = /*#__PURE__*/ createUseWriteContract({ + abi: saleAbi, + functionName: 'setToken', +}) + /** * Wraps __{@link useWriteContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"withdraw"` */ @@ -20171,6 +20281,14 @@ export const useSimulateSaleBuy = /*#__PURE__*/ createUseSimulateContract({ functionName: 'buy', }) +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"claim"` + */ +export const useSimulateSaleClaim = /*#__PURE__*/ createUseSimulateContract({ + abi: saleAbi, + functionName: 'claim', +}) + /** * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"grantRole"` */ @@ -20240,6 +20358,14 @@ export const useSimulateSaleSetMinContribution = functionName: 'setMinContribution', }) +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"setToken"` + */ +export const useSimulateSaleSetToken = /*#__PURE__*/ createUseSimulateContract({ + abi: saleAbi, + functionName: 'setToken', +}) + /** * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link saleAbi}__ and `functionName` set to `"withdraw"` */ @@ -20255,6 +20381,13 @@ export const useWatchSaleEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: saleAbi, }) +/** + * Wraps __{@link useWatchContractEvent}__ with `abi` set to __{@link saleAbi}__ and `eventName` set to `"Claim"` + */ +export const useWatchSaleClaimEvent = /*#__PURE__*/ createUseWatchContractEvent( + { abi: saleAbi, eventName: 'Claim' }, +) + /** * Wraps __{@link useWatchContractEvent}__ with `abi` set to __{@link saleAbi}__ and `eventName` set to `"Purchase"` */ @@ -20443,6 +20576,15 @@ export const useWriteSaleTestTestBuyAboveMaximum = functionName: 'testBuyAboveMaximum', }) +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link saleTestAbi}__ and `functionName` set to `"testBuyAndClaim"` + */ +export const useWriteSaleTestTestBuyAndClaim = + /*#__PURE__*/ createUseWriteContract({ + abi: saleTestAbi, + functionName: 'testBuyAndClaim', + }) + /** * Wraps __{@link useWriteContract}__ with `abi` set to __{@link saleTestAbi}__ and `functionName` set to `"testBuyBelowMinimum"` */ @@ -20502,6 +20644,15 @@ export const useSimulateSaleTestTestBuyAboveMaximum = functionName: 'testBuyAboveMaximum', }) +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link saleTestAbi}__ and `functionName` set to `"testBuyAndClaim"` + */ +export const useSimulateSaleTestTestBuyAndClaim = + /*#__PURE__*/ createUseSimulateContract({ + abi: saleTestAbi, + functionName: 'testBuyAndClaim', + }) + /** * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link saleTestAbi}__ and `functionName` set to `"testBuyBelowMinimum"` */ @@ -20527,6 +20678,15 @@ export const useWatchSaleTestEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: saleTestAbi, }) +/** + * Wraps __{@link useWatchContractEvent}__ with `abi` set to __{@link saleTestAbi}__ and `eventName` set to `"Claim"` + */ +export const useWatchSaleTestClaimEvent = + /*#__PURE__*/ createUseWatchContractEvent({ + abi: saleTestAbi, + eventName: 'Claim', + }) + /** * Wraps __{@link useWatchContractEvent}__ with `abi` set to __{@link saleTestAbi}__ and `eventName` set to `"Purchase"` */