diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e4c3a36 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +FORGE_ACCOUNT= # Only needed if you want to deploy with a forge account instead of a private key +BASESCAN_API_KEY= # Only needed if you want to verify the contract on Basescan + +BASE_TOKEN_NAME= +BASE_TOKEN_SYMBOL= +BASE_BENEFICIARY= +BASE_RPC_URL= +BASE_CONTROL= +BASE_FEE_COLLECTOR= +BASE_PRICE_INCREASE= # Price increase in ether (wei), e.g. 200000000000000 for 0.0002 ETH + +BASE_SEPOLIA_TOKEN_NAME= +BASE_SEPOLIA_TOKEN_SYMBOL= +BASE_SEPOLIA_BENEFICIARY= +BASE_SEPOLIA_RPC_URL= +BASE_SEPOLIA_CONTROL= +BASE_SEPOLIA_FEE_COLLECTOR= +BASE_SEPOLIA_PRICE_INCREASE= # Price increase in ether (wei), e.g. 200000000000000 for 0.0002 ETH \ No newline at end of file diff --git a/deploy/base-sepolia.sh b/deploy/base-sepolia.sh index 01e61f3..1f990b6 100755 --- a/deploy/base-sepolia.sh +++ b/deploy/base-sepolia.sh @@ -1 +1,25 @@ -forge script ./script/DeployAgentKey.s.sol --rpc-url https://sepolia.base.org/ --broadcast --interactives 1 -g 200 --force --verify --verifier-url https://api-sepolia.basescan.org/api --etherscan-api-key $1 \ No newline at end of file +if [[ $1 = "pk" ]]; then + export $(cat .env | xargs) && \ + forge script ./script/DeployAgentKey.s.sol \ + --rpc-url $BASE_SEPOLIA_RPC_URL \ + --broadcast \ + -g 200 \ + --force \ + --verify \ + --verifier-url https://api-sepolia.basescan.org/api \ + --etherscan-api-key $BASESCAN_API_KEY \ + --interactives 1 \ + --slow +else + export $(cat .env | xargs) && \ + forge script ./script/DeployAgentKey.s.sol \ + --rpc-url $BASE_SEPOLIA_RPC_URL \ + --broadcast \ + -g 200 \ + --force \ + --verify \ + --verifier-url https://api-sepolia.basescan.org/api \ + --etherscan-api-key $BASESCAN_API_KEY \ + --account $FORGE_ACCOUNT \ + --slow +fi \ No newline at end of file diff --git a/deploy/base.sh b/deploy/base.sh new file mode 100755 index 0000000..1da4250 --- /dev/null +++ b/deploy/base.sh @@ -0,0 +1,25 @@ +if [[ $1 = "pk" ]]; then + export $(cat .env | xargs) && \ + forge script ./script/DeployAgentKey.s.sol \ + --rpc-url $BASE_RPC_URL \ + --broadcast \ + -g 200 \ + --force \ + --verify \ + --verifier-url https://api.basescan.org/api \ + --etherscan-api-key $BASESCAN_API_KEY \ + --interactives 1 \ + --slow +else + export $(cat .env | xargs) && \ + forge script ./script/DeployAgentKey.s.sol \ + --rpc-url $BASE_RPC_URL \ + --broadcast \ + -g 200 \ + --force \ + --verify \ + --verifier-url https://api.basescan.org/api \ + --etherscan-api-key $BASESCAN_API_KEY \ + --account $FORGE_ACCOUNT \ + --slow +fi \ No newline at end of file diff --git a/deploy/local.sh b/deploy/local.sh deleted file mode 100755 index c4b9fc1..0000000 --- a/deploy/local.sh +++ /dev/null @@ -1 +0,0 @@ -forge script ./script/DeployAgentKey.s.sol --rpc-url http://localhost:8545 --gas-estimate-multiplier 200 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ No newline at end of file diff --git a/package.json b/package.json index c6bae08..4a7a3cd 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,6 @@ "scripts": {}, "dependencies": {}, "devDependencies": { - "@fairmint/c-org-contracts": "^2.4.24" + "@fairmint/c-org-contracts": "git+https://github.com/Fairmint/c-org#1770aa5d527175a676269ad80f458c46b30456ac" } } diff --git a/script/DeployAgentKey.s.sol b/script/DeployAgentKey.s.sol index d2c3274..505870c 100644 --- a/script/DeployAgentKey.s.sol +++ b/script/DeployAgentKey.s.sol @@ -16,47 +16,38 @@ contract DeployAgentKey is Script { deploy(helper.getConfig()); } - function deploy( - HelperConfig.AgentKeyConfig memory config - ) public returns (IAgentKey key, address whitelist) { + function deploy(HelperConfig.AgentKeyConfig memory config) public returns (IAgentKey key, address whitelist) { { - uint256 initReserve = 0 ether; - address currencyAddress = address(0); - uint256 initGoal = 0; - uint256 setupFee = 0; - address payable setupFeeRecipient = payable(address(0)); - string memory name = "Agent Keys"; - string memory symbol = "KEYS"; - + // buySlopeNum and buySlopeDen are used for the formula in: https://github.com/Fairmint/c-org/blob/781d1ed8d70d733eed57c5e7fff8931b096de0e9/contracts/ContinuousOffering.sol#L495 bytes memory ctorArgs = abi.encode( - initReserve, - currencyAddress, - initGoal, - config.buySlopeNum, - config.buySlopeDen, + 0 ether, // initReserve + address(0), // currencyAddress + 0, // initGoal + 2, // buySlopeNum + 50000000 * config.priceIncrease, // buySlopeDen config.investmentReserveBasisPoints, - setupFee, - setupFeeRecipient, - name, - symbol - ); + 0, // setupFee + payable(address(0)), // setupFeeRecipient + config.name, + config.symbol + ); vm.startBroadcast(); - + key = IAgentKey(deployCode("AgentKey.sol:AgentKey", ctorArgs)); } - + whitelist = address(new AgentKeyWhitelist()); - + key.updateConfig( - whitelist, + whitelist, config.beneficiary, config.control, config.feeCollector, config.feeBasisPoints, config.revenueCommitmentBasisPoints, - 1, - 0 + 1, // minInvestment + 0 // minDuration ); vm.stopBroadcast(); diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol index b187c27..8b8089d 100644 --- a/script/HelperConfig.s.sol +++ b/script/HelperConfig.s.sol @@ -4,64 +4,62 @@ pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; abstract contract Constants { - uint256 public constant CHAIN_ID_LOCAL = 31337; + uint256 public constant CHAIN_ID_BASE = 8453; uint256 public constant CHAIN_ID_BASE_SEPOLIA = 84532; } contract HelperConfig is Constants, Script { struct AgentKeyConfig { - uint256 buySlopeNum; - uint256 buySlopeDen; + string name; + string symbol; + uint256 priceIncrease; uint256 investmentReserveBasisPoints; - uint feeBasisPoints; - uint revenueCommitmentBasisPoints; + uint256 feeBasisPoints; + uint256 revenueCommitmentBasisPoints; address payable beneficiary; address control; address payable feeCollector; } - AgentKeyConfig public localAgentKeyConfig; - mapping (uint256 chainId => AgentKeyConfig) public agentKeyConfigs; - - constructor() { - agentKeyConfigs[CHAIN_ID_LOCAL] = getLocalAnvilConfig(); - agentKeyConfigs[CHAIN_ID_BASE_SEPOLIA] = getBaseSepoliaConfig(); - } - function getConfig() public view returns (AgentKeyConfig memory) { return getConfigByChainId(block.chainid); } function getConfigByChainId(uint256 chainId) private view returns (AgentKeyConfig memory) { - return agentKeyConfigs[chainId]; + if (chainId == CHAIN_ID_BASE_SEPOLIA) { + return getBaseSepoliaConfig(); + } else if (chainId == CHAIN_ID_BASE) { + return getBaseConfig(); + } else { + revert("Unsupported chain id"); + } } - function getLocalAnvilConfig() private pure returns (AgentKeyConfig memory) { + function getBaseConfig() private view returns (AgentKeyConfig memory) { return AgentKeyConfig({ - buySlopeNum: 2, - buySlopeDen: 10000 * 1e18, - investmentReserveBasisPoints: 9500, - // First address derived from mnemonic: test test test test test test test test test test test junk - beneficiary: payable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266), - // Second address - control: address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8), - // Third address - feeCollector: payable(0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC), - feeBasisPoints: 1000, + name: vm.envString("BASE_TOKEN_NAME"), + symbol: vm.envString("BASE_TOKEN_SYMBOL"), + priceIncrease: vm.envUint("BASE_PRICE_INCREASE"), + investmentReserveBasisPoints: 9000, + beneficiary: payable(vm.envAddress("BASE_BENEFICIARY")), + control: vm.envAddress("BASE_CONTROL"), + feeCollector: payable(vm.envAddress("BASE_FEE_COLLECTOR")), + feeBasisPoints: 5000, revenueCommitmentBasisPoints: 9500 }); } - function getBaseSepoliaConfig() private pure returns (AgentKeyConfig memory) { + function getBaseSepoliaConfig() private view returns (AgentKeyConfig memory) { return AgentKeyConfig({ - buySlopeNum: 2, - buySlopeDen: 10000 * 1e18, - investmentReserveBasisPoints: 9500, - beneficiary: payable(0x857766085629c1d68704989974A968cbdbf2fc3f), - control: 0x857766085629c1d68704989974A968cbdbf2fc3f, - feeCollector: payable(0x857766085629c1d68704989974A968cbdbf2fc3f), - feeBasisPoints: 1000, + name: vm.envString("BASE_SEPOLIA_TOKEN_NAME"), + symbol: vm.envString("BASE_SEPOLIA_TOKEN_SYMBOL"), + priceIncrease: vm.envUint("BASE_SEPOLIA_PRICE_INCREASE"), + investmentReserveBasisPoints: 9000, + beneficiary: payable(vm.envAddress("BASE_SEPOLIA_BENEFICIARY")), + control: vm.envAddress("BASE_SEPOLIA_CONTROL"), + feeCollector: payable(vm.envAddress("BASE_SEPOLIA_FEE_COLLECTOR")), + feeBasisPoints: 5000, revenueCommitmentBasisPoints: 9500 }); } -} \ No newline at end of file +} diff --git a/src/AgentKey.sol b/src/AgentKey.sol index e095a9c..ff19402 100644 --- a/src/AgentKey.sol +++ b/src/AgentKey.sol @@ -2,16 +2,19 @@ pragma solidity 0.5.17; import {DecentralizedAutonomousTrust} from "@fairmint/contracts/DecentralizedAutonomousTrust.sol"; +import "@openzeppelin/contracts-ethereum-package/contracts/utils/Address.sol"; contract AgentKey is DecentralizedAutonomousTrust { + bool public isStopped; + constructor( - uint _initReserve, + uint256 _initReserve, address _currencyAddress, - uint _initGoal, - uint _buySlopeNum, - uint _buySlopeDen, - uint _investmentReserveBasisPoints, - uint _setupFee, + uint256 _initGoal, + uint256 _buySlopeNum, + uint256 _buySlopeDen, + uint256 _investmentReserveBasisPoints, + uint256 _setupFee, address payable _setupFeeRecipient, string memory _name, string memory _symbol @@ -26,7 +29,31 @@ contract AgentKey is DecentralizedAutonomousTrust { _setupFee, _setupFeeRecipient, _name, - _symbol + _symbol ); } + + /// @notice Stops the contract and transfers the reserve to the recipient. To be used in case of a migration to a new contract. + function stopAndTransferReserve(address payable _recipient) external { + require(msg.sender == beneficiary, "BENEFICIARY_ONLY"); + isStopped = true; + Address.sendValue(_recipient, address(this).balance); + } + + /// @dev Overrides the modifier in ContinuousOffering + modifier authorizeTransfer( + address _from, + address _to, + uint256 _value, + bool _isSell + ) { + if (isStopped) { + revert("Contract is stopped"); + } + if (address(whitelist) != address(0)) { + // This is not set for the minting of initialReserve + whitelist.authorizeTransfer(_from, _to, _value, _isSell); + } + _; + } } diff --git a/src/AgentKeyWhitelist.sol b/src/AgentKeyWhitelist.sol index 6ea9fec..7070ad0 100644 --- a/src/AgentKeyWhitelist.sol +++ b/src/AgentKeyWhitelist.sol @@ -2,15 +2,7 @@ pragma solidity ^0.8.13; contract AgentKeyWhitelist { - function authorizeTransfer( - address _from, - address _to, - uint, - bool - ) external pure { - require( - _from == address(0) || _to == address(0), - "TRANSFERS_DISABLED" - ); + function authorizeTransfer(address _from, address _to, uint256, bool) external pure { + require(_from == address(0) || _to == address(0), "TRANSFERS_DISABLED"); } -} \ No newline at end of file +} diff --git a/src/IAgentKey.sol b/src/IAgentKey.sol index bb17931..60edebb 100644 --- a/src/IAgentKey.sol +++ b/src/IAgentKey.sol @@ -3,22 +3,10 @@ pragma solidity ^0.8.13; interface IAgentKey { function approve(address spender, uint256 amount) external returns (bool); - function buy( - address _to, - uint256 _currencyValue, - uint256 _minTokensBought - ) external payable; - function sell( - address payable _to, - uint _quantityToSell, - uint _minCurrencyReturned - ) external; - function estimateBuyValue( - uint256 _currencyValue - ) external view returns (uint256); - function estimateSellValue( - uint _quantityToSell - ) external view returns(uint256); + function buy(address _to, uint256 _currencyValue, uint256 _minTokensBought) external payable; + function sell(address payable _to, uint256 _quantityToSell, uint256 _minCurrencyReturned) external; + function estimateBuyValue(uint256 _currencyValue) external view returns (uint256); + function estimateSellValue(uint256 _quantityToSell) external view returns (uint256); function balanceOf(address _owner) external view returns (uint256); function state() external view returns (uint256); function updateConfig( @@ -26,15 +14,17 @@ interface IAgentKey { address payable _beneficiary, address _control, address payable _feeCollector, - uint _feeBasisPoints, - uint _revenueCommitmentBasisPoints, - uint _minInvestment, - uint _minDuration + uint256 _feeBasisPoints, + uint256 _revenueCommitmentBasisPoints, + uint256 _minInvestment, + uint256 _minDuration ) external; - function pay(uint _currencyValue) external payable; + function pay(uint256 _currencyValue) external payable; function totalSupply() external view returns (uint256); function buybackReserve() external view returns (uint256); function feeBasisPoints() external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function close() external; -} \ No newline at end of file + function stopAndTransferReserve(address payable _recipient) external; + function isStopped() external view returns (bool); +} diff --git a/test/AgentKey.t.sol b/test/AgentKey.t.sol index 605e5d0..ee9e5e5 100644 --- a/test/AgentKey.t.sol +++ b/test/AgentKey.t.sol @@ -16,16 +16,17 @@ contract AgentKeyTest is Test { address user = makeAddr("user"); address recipient = makeAddr("recipient"); - function setUp() public { + function setUp() public { DeployAgentKey keyDeployer = new DeployAgentKey(); (key, whitelist) = keyDeployer.deploy( HelperConfig.AgentKeyConfig({ - buySlopeNum: 2, - buySlopeDen: 10000 * 1e18, - investmentReserveBasisPoints: 9500, - feeBasisPoints: 2000, - revenueCommitmentBasisPoints: 9000, + name: "Agent keys", + symbol: "KEY", + priceIncrease: 0.0002 * 1e18, + investmentReserveBasisPoints: 9000, + feeBasisPoints: 5000, + revenueCommitmentBasisPoints: 9500, beneficiary: payable(beneficiary), control: control, feeCollector: payable(feeCollector) @@ -34,10 +35,10 @@ contract AgentKeyTest is Test { } function test_canBuyTokens() public { - uint amountToSpend = 1 ether; - uint expectedBeneficiaryFee = 0.04 ether; - uint expectedFeeCollectorFee = 0.01 ether; - uint expectedReserve = 0.95 ether; + uint256 amountToSpend = 1 ether; + uint256 expectedBeneficiaryFee = 0.05 ether; + uint256 expectedFeeCollectorFee = 0.05 ether; + uint256 expectedReserve = 0.9 ether; vm.deal(user, amountToSpend); @@ -47,18 +48,14 @@ contract AgentKeyTest is Test { vm.startPrank(user); - uint minBuyAmount = key.estimateBuyValue(amountToSpend); + uint256 minBuyAmount = key.estimateBuyValue(amountToSpend); assertGt(minBuyAmount, 0); - - key.buy{ - value: amountToSpend - }( - user, amountToSpend, minBuyAmount - ); - uint balance = key.balanceOf(user); + key.buy{value: amountToSpend}(user, amountToSpend, minBuyAmount); - assertGt(balance, 0); + uint256 balance = key.balanceOf(user); + + assertGe(balance, minBuyAmount); assertEq(key.balanceOf(address(beneficiary)), 0); assertEq(key.balanceOf(address(feeCollector)), 0); @@ -69,12 +66,32 @@ contract AgentKeyTest is Test { assertEq(key.buybackReserve(), expectedReserve); } - function test_curveBehavesAccordingToFormula() public { + function test_curveBehavesAccordingToFormula1() public { + // Initial price is 100 KEY for 1 ETH + // Formula: price = (tokens ** 2) / 2 * buySlopeNum / buySlopeDen + + uint256 amountToSpend = 600 ether; + + vm.deal(user, amountToSpend); + + assertEq(key.balanceOf(user), 0); + assertEq(beneficiary.balance, 0); + assertEq(feeCollector.balance, 0); + + vm.startPrank(user); + + uint256 minBuyAmount1 = key.estimateBuyValue(100 ether); + assertEq(minBuyAmount1, 1000 ether); // 1000 KEY tokens for 100 ETH + + key.buy{value: 100 ether}(user, 100 ether, minBuyAmount1); + } + + function test_curveBehavesAccordingToFormula2() public { // Initial price is 100 KEY for 1 ETH // Formula: price = (tokens ** 2) / 2 * buySlopeNum / buySlopeDen - uint amountToSpend = 600 ether; - + uint256 amountToSpend = 600 ether; + vm.deal(user, amountToSpend); assertEq(key.balanceOf(user), 0); @@ -83,42 +100,30 @@ contract AgentKeyTest is Test { vm.startPrank(user); - uint minBuyAmount1 = key.estimateBuyValue(100 ether); + uint256 minBuyAmount1 = key.estimateBuyValue(100 ether); assertEq(minBuyAmount1, 1000 ether); - - key.buy{ - value: 100 ether - }( - user, 100 ether, minBuyAmount1 - ); - uint balance1 = key.balanceOf(user); + key.buy{value: 100 ether}(user, 100 ether, minBuyAmount1); + + uint256 balance1 = key.balanceOf(user); assertEq(balance1, 1000 ether); - uint minBuyAmount2 = key.estimateBuyValue(200 ether); + uint256 minBuyAmount2 = key.estimateBuyValue(200 ether); assertEq(minBuyAmount2, 732.050807568877293527 ether); - key.buy{ - value: 200 ether - }( - user, 200 ether, minBuyAmount2 - ); + key.buy{value: 200 ether}(user, 200 ether, minBuyAmount2); - uint balance2 = key.balanceOf(user); + uint256 balance2 = key.balanceOf(user); assertEq(balance2, 1732.050807568877293527 ether); - uint minBuyAmount3 = key.estimateBuyValue(300 ether); + uint256 minBuyAmount3 = key.estimateBuyValue(300 ether); assertEq(minBuyAmount3, 717.438935214300804669 ether); - key.buy{ - value: 300 ether - }( - user, 300 ether, minBuyAmount3 - ); + key.buy{value: 300 ether}(user, 300 ether, minBuyAmount3); - uint balance3 = key.balanceOf(user); + uint256 balance3 = key.balanceOf(user); assertEq(balance3, 2449.489742783178098196 ether); @@ -126,24 +131,20 @@ contract AgentKeyTest is Test { } function test_priceIncreasesWhenSupplyIncreases() public { - uint amountToSpend = 1 ether; - uint expectedBeneficiaryFee = 0.04 ether; - uint expectedFeeCollectorFee = 0.01 ether; - uint expectedReserve = 0.95 ether; + uint256 amountToSpend = 1 ether; + uint256 expectedBeneficiaryFee = 0.05 ether; + uint256 expectedFeeCollectorFee = 0.05 ether; + uint256 expectedReserve = 0.9 ether; vm.deal(user, amountToSpend); vm.startPrank(user); - uint minBuyAmount1 = key.estimateBuyValue(amountToSpend / 2); + uint256 minBuyAmount1 = key.estimateBuyValue(amountToSpend / 2); assertGt(minBuyAmount1, 0); - key.buy{ - value: amountToSpend / 2 - }( - user, amountToSpend / 2, minBuyAmount1 - ); + key.buy{value: amountToSpend / 2}(user, amountToSpend / 2, minBuyAmount1); - uint balance1 = key.balanceOf(user); + uint256 balance1 = key.balanceOf(user); assertGt(balance1, 0); @@ -153,17 +154,13 @@ contract AgentKeyTest is Test { assertEq(key.totalSupply(), balance1); assertEq(key.buybackReserve(), expectedReserve / 2); - uint minBuyAmount2 = key.estimateBuyValue(amountToSpend / 2); + uint256 minBuyAmount2 = key.estimateBuyValue(amountToSpend / 2); assertGt(minBuyAmount2, 0); assertLt(minBuyAmount2, minBuyAmount1); - key.buy{ - value: amountToSpend / 2 - }( - user, amountToSpend / 2, minBuyAmount2 - ); + key.buy{value: amountToSpend / 2}(user, amountToSpend / 2, minBuyAmount2); - uint balance2 = key.balanceOf(user); + uint256 balance2 = key.balanceOf(user); assertGt(balance2, 0); assertGt(balance2 - balance1, 0); @@ -178,21 +175,17 @@ contract AgentKeyTest is Test { } function test_transfersAreDisabled() public { - uint amountToSpend = 1 ether; - + uint256 amountToSpend = 1 ether; + vm.deal(user, amountToSpend); - - uint minBuyAmount = key.estimateBuyValue(amountToSpend); + + uint256 minBuyAmount = key.estimateBuyValue(amountToSpend); assertGt(minBuyAmount, 0); vm.startPrank(user); - key.buy{ - value: amountToSpend - }( - user, amountToSpend, 1 - ); + key.buy{value: amountToSpend}(user, amountToSpend, 1); - uint userBalance = key.balanceOf(user); + uint256 userBalance = key.balanceOf(user); assertGe(userBalance, minBuyAmount); vm.expectRevert("TRANSFERS_DISABLED"); @@ -203,13 +196,9 @@ contract AgentKeyTest is Test { vm.deal(beneficiary, amountToSpend); vm.startPrank(beneficiary); - key.buy{ - value: amountToSpend - }( - beneficiary, amountToSpend, 1 - ); + key.buy{value: amountToSpend}(beneficiary, amountToSpend, 1); - uint beneficiaryBalance = key.balanceOf(beneficiary); + uint256 beneficiaryBalance = key.balanceOf(beneficiary); assertGe(beneficiaryBalance, key.estimateBuyValue(amountToSpend)); vm.expectRevert("TRANSFERS_DISABLED"); @@ -221,27 +210,23 @@ contract AgentKeyTest is Test { } function test_canSellTokens() public { - uint amountToSpend = 1 ether; - uint expectedBeneficiaryFee = 0.04 ether; - uint expectedFeeCollectorFee = 0.01 ether; - uint expectedReserve = 0.95 ether; + uint256 amountToSpend = 1 ether; + uint256 expectedBeneficiaryFee = 0.05 ether; + uint256 expectedFeeCollectorFee = 0.05 ether; + uint256 expectedReserve = 0.9 ether; // Some of the buybackReserve is left over even after selling all tokens // Most likely due to rounding errors or because of the fees - uint expectedMaxReserveAfterSell = 0.001 ether; + uint256 expectedMaxReserveAfterSell = 0.001 ether; vm.deal(user, amountToSpend); - - uint minBuyAmount = key.estimateBuyValue(amountToSpend); + + uint256 minBuyAmount = key.estimateBuyValue(amountToSpend); assertGt(minBuyAmount, 0); vm.startPrank(user); - key.buy{ - value: amountToSpend - }( - user, amountToSpend, minBuyAmount - ); + key.buy{value: amountToSpend}(user, amountToSpend, minBuyAmount); - uint balance = key.balanceOf(user); + uint256 balance = key.balanceOf(user); assertGt(balance, 0); @@ -249,11 +234,7 @@ contract AgentKeyTest is Test { assertEq(feeCollector.balance, expectedFeeCollectorFee); assertEq(key.buybackReserve(), expectedReserve); - key.sell( - payable(user), - balance, - 1 - ); + key.sell(payable(user), balance, 1); assertEq(key.balanceOf(user), 0); @@ -265,24 +246,20 @@ contract AgentKeyTest is Test { } function test_priceDecreasesWhenSupplyDecreases() public { - uint amountToSpend = 1 ether; - uint expectedBeneficiaryFee = 0.04 ether; - uint expectedFeeCollectorFee = 0.01 ether; - uint expectedReserve = 0.95 ether; + uint256 amountToSpend = 1 ether; + uint256 expectedBeneficiaryFee = 0.05 ether; + uint256 expectedFeeCollectorFee = 0.05 ether; + uint256 expectedReserve = 0.9 ether; vm.deal(user, amountToSpend); vm.startPrank(user); - uint minBuyAmount1 = key.estimateBuyValue(amountToSpend / 2); + uint256 minBuyAmount1 = key.estimateBuyValue(amountToSpend / 2); assertGt(minBuyAmount1, 0); - - key.buy{ - value: amountToSpend / 2 - }( - user, amountToSpend / 2, minBuyAmount1 - ); - uint balance1 = key.balanceOf(user); + key.buy{value: amountToSpend / 2}(user, amountToSpend / 2, minBuyAmount1); + + uint256 balance1 = key.balanceOf(user); assertGt(balance1, 0); @@ -292,38 +269,30 @@ contract AgentKeyTest is Test { assertEq(key.totalSupply(), balance1); assertEq(key.buybackReserve(), expectedReserve / 2); - uint minBuyAmount2 = key.estimateBuyValue(amountToSpend / 2); + uint256 minBuyAmount2 = key.estimateBuyValue(amountToSpend / 2); assertGt(minBuyAmount2, 0); assertLt(minBuyAmount2, minBuyAmount1); - key.buy{ - value: amountToSpend / 2 - }( - user, amountToSpend / 2, minBuyAmount2 - ); + key.buy{value: amountToSpend / 2}(user, amountToSpend / 2, minBuyAmount2); - uint balance2 = key.balanceOf(user); + uint256 balance2 = key.balanceOf(user); - uint minBuyAmount3 = key.estimateBuyValue(amountToSpend / 2); + uint256 minBuyAmount3 = key.estimateBuyValue(amountToSpend / 2); assertLt(minBuyAmount3, minBuyAmount2); - key.sell( - payable(user), - balance2 - balance1, - 1 - ); + key.sell(payable(user), balance2 - balance1, 1); assertEq(key.balanceOf(user), balance1); assertEq(key.totalSupply(), balance1); - uint minBuyAmount4 = key.estimateBuyValue(amountToSpend / 2); + uint256 minBuyAmount4 = key.estimateBuyValue(amountToSpend / 2); assertGt(minBuyAmount4, minBuyAmount3); } function test_pay() public { - uint amountToPay = 10 ether; - uint revenueFee = amountToPay * 1000 / 10000; // 10% - uint expectedReserve = 9 ether; + uint256 amountToPay = 10 ether; + uint256 revenueFee = amountToPay * 5 / 100; // 5% + uint256 expectedReserve = 9.5 ether; vm.deal(user, amountToPay); @@ -336,10 +305,8 @@ contract AgentKeyTest is Test { assertEq(feeCollector.balance, 0); vm.startPrank(user); - key.pay{ - value: amountToPay - }(amountToPay); - + key.pay{value: amountToPay}(amountToPay); + assertEq(key.totalSupply(), 0); assertEq(key.buybackReserve(), expectedReserve); @@ -349,8 +316,8 @@ contract AgentKeyTest is Test { } function test_payByTransfer() public { - uint amountToPay = 10 ether; - uint expectedReserve = amountToPay; + uint256 amountToPay = 10 ether; + uint256 expectedReserve = amountToPay; vm.deal(user, amountToPay); @@ -364,7 +331,7 @@ contract AgentKeyTest is Test { vm.startPrank(user); payable(address(key)).transfer(amountToPay); - + assertEq(key.totalSupply(), 0); assertEq(key.buybackReserve(), expectedReserve); @@ -373,56 +340,44 @@ contract AgentKeyTest is Test { } function test_sellPriceIncreasesAfterPay() public { - uint amountForBuy = 1 ether; - uint amountToPay = 10 ether; + uint256 amountForBuy = 1 ether; + uint256 amountToPay = 10 ether; vm.deal(user, amountForBuy + amountToPay); vm.startPrank(user); - uint minBuyAmount = key.estimateBuyValue(amountForBuy); + uint256 minBuyAmount = key.estimateBuyValue(amountForBuy); assertGt(minBuyAmount, 0); - - key.buy{ - value: amountForBuy - }( - user, amountForBuy, minBuyAmount - ); - uint minSellAmount = key.estimateSellValue(minBuyAmount); + key.buy{value: amountForBuy}(user, amountForBuy, minBuyAmount); + + uint256 minSellAmount = key.estimateSellValue(minBuyAmount); assertGt(minSellAmount, 0); - key.pay{ - value: amountToPay - }(amountToPay); + key.pay{value: amountToPay}(amountToPay); - uint minSellAmountAfterPay = key.estimateSellValue(minBuyAmount); + uint256 minSellAmountAfterPay = key.estimateSellValue(minBuyAmount); assertGt(minSellAmountAfterPay, minSellAmount); } - function test_buyPriceRemainsSameAfterPay() public { - uint amountForBuy = 1 ether; - uint amountToPay = 10 ether; + function test_buyPriceRemainsSameAfterPay() public { + uint256 amountForBuy = 1 ether; + uint256 amountToPay = 10 ether; vm.deal(user, amountForBuy + amountToPay); vm.startPrank(user); - uint minBuyAmount = key.estimateBuyValue(amountForBuy); + uint256 minBuyAmount = key.estimateBuyValue(amountForBuy); assertGt(minBuyAmount, 0); - key.buy{ - value: amountForBuy - }( - user, amountForBuy, minBuyAmount - ); + key.buy{value: amountForBuy}(user, amountForBuy, minBuyAmount); - uint minBuyAmountBeforePay = key.estimateBuyValue(amountForBuy); + uint256 minBuyAmountBeforePay = key.estimateBuyValue(amountForBuy); assertGt(minBuyAmountBeforePay, 0); - key.pay{ - value: amountToPay - }(amountToPay); + key.pay{value: amountToPay}(amountToPay); - uint minBuyAmountAfterPay = key.estimateBuyValue(amountForBuy); + uint256 minBuyAmountAfterPay = key.estimateBuyValue(amountForBuy); assertEq(minBuyAmountAfterPay, minBuyAmountBeforePay); } @@ -442,40 +397,91 @@ contract AgentKeyTest is Test { function test_onlyControlCanUpdateConfig() public { vm.prank(user); vm.expectRevert("CONTROL_ONLY"); - key.updateConfig( - whitelist, - payable(beneficiary), - payable(control), - payable(feeCollector), - 0, - 9000, - 1, - 0 - ); + key.updateConfig(whitelist, payable(beneficiary), payable(control), payable(feeCollector), 0, 9500, 1, 0); vm.prank(beneficiary); vm.expectRevert("CONTROL_ONLY"); - key.updateConfig( - whitelist, - payable(beneficiary), - payable(control), - payable(feeCollector), - 0, - 9000, - 1, - 0 - ); + key.updateConfig(whitelist, payable(beneficiary), payable(control), payable(feeCollector), 0, 9500, 1, 0); vm.prank(control); - key.updateConfig( - whitelist, - payable(beneficiary), - payable(control), - payable(feeCollector), - 0, - 9000, - 1, - 0 - ); + key.updateConfig(whitelist, payable(beneficiary), payable(control), payable(feeCollector), 0, 9500, 1, 0); + } + + function test_contractCanBeStopped() public { + vm.prank(beneficiary); + key.stopAndTransferReserve(payable(recipient)); + assertEq(key.isStopped(), true); + } + + function test_buysAndSellsAreDisabledWhenContractIsStopped() public { + vm.deal(user, 2 ether); + vm.prank(user); + uint256 minBuyAmount = key.estimateBuyValue(1 ether); + + key.buy{value: 1 ether}(user, 1 ether, minBuyAmount); + + vm.prank(beneficiary); + key.stopAndTransferReserve(payable(recipient)); + + vm.prank(user); + vm.expectRevert("Contract is stopped"); + key.buy{value: 1 ether}(user, 1 ether, 1); + + vm.prank(user); + vm.expectRevert("PRICE_SLIPPAGE"); // Error is PRICE_SLIPPAGE because the reserve check is done before the stopped check + key.sell(payable(user), 1 ether, 1); + } + + function test_reserveIsTransferredAfterStop() public { + vm.deal(user, 2 ether); + vm.prank(user); + uint256 minBuyAmount = key.estimateBuyValue(1 ether); + + key.buy{value: 1 ether}(user, 1 ether, minBuyAmount); + + uint256 reserveBefore = key.buybackReserve(); + + assertGt(reserveBefore, 0); + assertEq(address(key).balance, reserveBefore); + + vm.prank(beneficiary); + key.stopAndTransferReserve(payable(recipient)); + + assertEq(key.buybackReserve(), 0); + assertEq(address(key).balance, 0); + + assertEq(recipient.balance, reserveBefore); + } + + function test_transfersAreDisabledWhenContractIsStopped() public { + vm.deal(user, 2 ether); + vm.prank(user); + uint256 minBuyAmount = key.estimateBuyValue(1 ether); + + key.buy{value: 1 ether}(user, 1 ether, minBuyAmount); + + vm.prank(beneficiary); + key.stopAndTransferReserve(payable(recipient)); + + vm.prank(user); + vm.expectRevert("Contract is stopped"); + key.transfer(makeAddr("new-recipient"), minBuyAmount); + } + + function test_onlyBeneficiaryCanStopTheContract() public { + assertEq(key.isStopped(), false); + + vm.prank(user); + vm.expectRevert("BENEFICIARY_ONLY"); + key.stopAndTransferReserve(payable(recipient)); + + vm.prank(control); + vm.expectRevert("BENEFICIARY_ONLY"); + key.stopAndTransferReserve(payable(recipient)); + + vm.prank(beneficiary); + key.stopAndTransferReserve(payable(recipient)); + + assertEq(key.isStopped(), true); } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 9c0b472..32bb623 100644 --- a/yarn.lock +++ b/yarn.lock @@ -507,10 +507,9 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@fairmint/c-org-contracts@^2.4.24": +"@fairmint/c-org-contracts@git+https://github.com/Fairmint/c-org#1770aa5d527175a676269ad80f458c46b30456ac": version "2.4.24" - resolved "https://registry.yarnpkg.com/@fairmint/c-org-contracts/-/c-org-contracts-2.4.24.tgz#b41f53e81b4d579c221492a212aa580a97bd6972" - integrity sha512-qvRz2WrdioJdDIe40+deNfOVlYsxTOAkxstHVvcdDGhrsJZwdDTVq0y7jDeFT36OQe0t5z2VFmFZ5qdoCIjC4A== + resolved "git+https://github.com/Fairmint/c-org#1770aa5d527175a676269ad80f458c46b30456ac" dependencies: "@openzeppelin/contracts" "2.5.1" "@openzeppelin/contracts-ethereum-package" "2.5.0"