diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4fbda5a --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +BASE_RPC= +OWNER_PRIVATE_KEY= +ETHERSCAN_API_KEY= \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09880b1..c14fdb1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,34 +1,25 @@ -name: test +name: CI -on: workflow_dispatch - -env: - FOUNDRY_PROFILE: ci +on: + push: + branches: + - main + pull_request: + branches: + - main jobs: - check: - strategy: - fail-fast: true - - name: Foundry project + build-and-test: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly + steps: + - name: Checkout repository + uses: actions/checkout@v2 - - name: Run Forge build - run: | - forge --version - forge build --sizes - id: build + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly - - name: Run Forge tests - run: | - forge test -vvv - id: test + - name: Run make test + run: make test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..690924b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d153856 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +-include .env + +.PHONY: install +install: + @foundryup + @forge install OpenZeppelin/openzeppelin-contracts --no-commit + +.PHONY: test +test: + @forge test -vvvv + +.PHONY: deploy +deploy: + @echo "Deploying to Base Mainnet" + @forge script script/Deploy.s.sol --rpc-url $(BASE_RPC) --private-key $(OWNER_PRIVATE_KEY) --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv + +.PHONY: build +build: + @forge build + +.PHONY: format +format: + @forge fmt \ No newline at end of file diff --git a/README.md b/README.md index 9265b45..d525cb7 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,21 @@ -## Foundry +## Friendly Agent -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +Helps to make limit orders within a single transaction on Friendtech. Some additional helpers added to withdraw ETH/Arbitrary ERC20s. -Foundry consists of: +Create `.env` file from `.env.example` and fill in the details. -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +## Install -## Documentation +`make install` -https://book.getfoundry.sh/ +## Tests -## Usage +`make test` -### Build +## Fork Tests -```shell -$ forge build -``` +`make test-fork` -### Test +## Deploy -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +`make deploy` diff --git a/foundry.toml b/foundry.toml index 25b918f..fd77e94 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,7 @@ src = "src" out = "out" libs = ["lib"] +remappings = ["@openzeppelin-contracts=lib/openzeppelin-contracts/contracts"] + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..1d9650e --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1d9650e951204a0ddce9ff89c32f1997984cef4d diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..fd81a96 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90 diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index 1a47b40..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console2} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/script/DeployFriendlyAgent.s.sol b/script/DeployFriendlyAgent.s.sol new file mode 100644 index 0000000..1ced40d --- /dev/null +++ b/script/DeployFriendlyAgent.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +import {Script} from "forge-std/Script.sol"; +import {FriendlyAgent} from "../src/FriendlyAgent.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; + +contract DeployFriendlyAgent is Script { + function run() external returns (FriendlyAgent) { + uint256 ownerPrivateKey = vm.envUint("OWNER_PRIVATE_KEY"); + HelperConfig helperConfig = new HelperConfig(); + + vm.startBroadcast(ownerPrivateKey); + FriendlyAgent friendlyAgent = new FriendlyAgent(helperConfig.friendsTechAddress()); + vm.stopBroadcast(); + + return friendlyAgent; + } +} diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol new file mode 100644 index 0000000..61ccb09 --- /dev/null +++ b/script/HelperConfig.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +import {Script} from "forge-std/Script.sol"; +import {FriendtechSharesV1} from "../src/FriendtechSharesV1.sol"; + +contract HelperConfig is Script { + address public friendsTechAddress; + + constructor() { + if (block.chainid == 8453) { + friendsTechAddress = getBaseMainnet(); + } else { + friendsTechAddress = getOrCreateAnvilConfig(); + } + } + + function getOrCreateAnvilConfig() public returns (address) { + if (friendsTechAddress != address(0)) { + return friendsTechAddress; + } + + vm.startBroadcast(); + FriendtechSharesV1 friendTechSharesV1 = new FriendtechSharesV1(); + friendTechSharesV1.setSubjectFeePercent(0); + friendTechSharesV1.setProtocolFeePercent(0); + + // Set to burn address for tests + friendTechSharesV1.setFeeDestination(0x0000000000000000000000000000000000000000); + vm.stopBroadcast(); + + return address(friendTechSharesV1); + } + + function getBaseMainnet() public pure returns (address) { + return 0xCF205808Ed36593aa40a44F10c7f7C2F67d4A4d4; + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/FriendlyAgent.sol b/src/FriendlyAgent.sol new file mode 100644 index 0000000..ef0a938 --- /dev/null +++ b/src/FriendlyAgent.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +interface IFriendtechSharesV1 { + function getBuyPriceAfterFee(address sharesSubject, uint256 amount) external view returns (uint256); + function getSellPriceAfterFee(address sharesSubject, uint256 amount) external view returns (uint256); + function buyShares(address sharesSubject, uint256 amount) external payable; + function sellShares(address sharesSubject, uint256 amount) external payable; +} + +interface IERC20 { + function transfer(address recipient, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); +} + +contract FriendlyAgent { + error FriendlyAgent__NotOwner(); + error FriendlyAgent__AmountGreaterThanHoldings(); + error FriendlyAgent__OverMaxLimit(); + error FriendlyAgent__UnderMinLimit(); + + IFriendtechSharesV1 public immutable i_friendtechShares; + address public immutable i_owner; + mapping(address => uint256) private s_holdings; + + constructor(address _friendtechSharesAddress) { + i_friendtechShares = IFriendtechSharesV1(_friendtechSharesAddress); + i_owner = msg.sender; + } + + modifier onlyOwner() { + if (msg.sender != i_owner) { + revert FriendlyAgent__NotOwner(); + } + _; + } + + function buyShares(uint256 maxPrice, uint256 amount, address sharesSubject) public payable onlyOwner { + uint256 priceAfterFee = i_friendtechShares.getBuyPriceAfterFee(sharesSubject, amount); + if (priceAfterFee > maxPrice) { + revert FriendlyAgent__OverMaxLimit(); + } + + s_holdings[sharesSubject] += amount; + i_friendtechShares.buyShares{value: priceAfterFee}(sharesSubject, amount); + } + + function sellShares(uint256 minPrice, uint256 amount, address sharesSubject) public onlyOwner { + if (s_holdings[sharesSubject] < amount) { + revert FriendlyAgent__AmountGreaterThanHoldings(); + } + uint256 priceAfterFee = i_friendtechShares.getSellPriceAfterFee(sharesSubject, amount); + if (priceAfterFee < minPrice) { + revert FriendlyAgent__UnderMinLimit(); + } + + s_holdings[sharesSubject] -= amount; + i_friendtechShares.sellShares{value: 0}(sharesSubject, amount); + } + + function withdraw() public onlyOwner { + (bool callSuccess,) = payable(i_owner).call{value: address(this).balance}(""); + require(callSuccess, "Call failed"); + } + + function withdrawToken(address tokenAddress) public onlyOwner { + IERC20 token = IERC20(tokenAddress); + uint256 balance = token.balanceOf(address(this)); + require(balance > 0, "No tokens to withdraw"); + require(token.transfer(i_owner, balance), "Token transfer failed"); + } + + function getHoldings(address sharesSubject) external view returns (uint256) { + return s_holdings[sharesSubject]; + } + + fallback() external payable {} + + receive() external payable {} +} diff --git a/src/FriendtechSharesV1.sol b/src/FriendtechSharesV1.sol new file mode 100644 index 0000000..1493c30 --- /dev/null +++ b/src/FriendtechSharesV1.sol @@ -0,0 +1,217 @@ +/** + * Submitted for verification at basescan.org on 2023-08-10 + */ + +// File: contracts/Context.sol + +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File: contracts/Ownable.sol + +// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _transferOwnership(_msgSender()); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} + +// File: contracts/FriendtechShares.sol + +pragma solidity >=0.8.2 <0.9.0; + +// TODO: Events, final pricing model, + +contract FriendtechSharesV1 is Ownable { + address public protocolFeeDestination; + uint256 public protocolFeePercent; + uint256 public subjectFeePercent; + + event Trade( + address trader, + address subject, + bool isBuy, + uint256 shareAmount, + uint256 ethAmount, + uint256 protocolEthAmount, + uint256 subjectEthAmount, + uint256 supply + ); + + // SharesSubject => (Holder => Balance) + mapping(address => mapping(address => uint256)) public sharesBalance; + + // SharesSubject => Supply + mapping(address => uint256) public sharesSupply; + + function setFeeDestination(address _feeDestination) public onlyOwner { + protocolFeeDestination = _feeDestination; + } + + function setProtocolFeePercent(uint256 _feePercent) public onlyOwner { + protocolFeePercent = _feePercent; + } + + function setSubjectFeePercent(uint256 _feePercent) public onlyOwner { + subjectFeePercent = _feePercent; + } + + function getPrice(uint256 supply, uint256 amount) public pure returns (uint256) { + uint256 sum1 = supply == 0 ? 0 : (supply - 1) * (supply) * (2 * (supply - 1) + 1) / 6; + uint256 sum2 = supply == 0 && amount == 1 + ? 0 + : (supply - 1 + amount) * (supply + amount) * (2 * (supply - 1 + amount) + 1) / 6; + uint256 summation = sum2 - sum1; + return summation * 1 ether / 16000; + } + + function getBuyPrice(address sharesSubject, uint256 amount) public view returns (uint256) { + return getPrice(sharesSupply[sharesSubject], amount); + } + + function getSellPrice(address sharesSubject, uint256 amount) public view returns (uint256) { + return getPrice(sharesSupply[sharesSubject] - amount, amount); + } + + function getBuyPriceAfterFee(address sharesSubject, uint256 amount) public view returns (uint256) { + uint256 price = getBuyPrice(sharesSubject, amount); + uint256 protocolFee = price * protocolFeePercent / 1 ether; + uint256 subjectFee = price * subjectFeePercent / 1 ether; + return price + protocolFee + subjectFee; + } + + function getSellPriceAfterFee(address sharesSubject, uint256 amount) public view returns (uint256) { + uint256 price = getSellPrice(sharesSubject, amount); + uint256 protocolFee = price * protocolFeePercent / 1 ether; + uint256 subjectFee = price * subjectFeePercent / 1 ether; + return price - protocolFee - subjectFee; + } + + function buyShares(address sharesSubject, uint256 amount) public payable { + uint256 supply = sharesSupply[sharesSubject]; + require(supply > 0 || sharesSubject == msg.sender, "Only the shares' subject can buy the first share"); + uint256 price = getPrice(supply, amount); + uint256 protocolFee = price * protocolFeePercent / 1 ether; + uint256 subjectFee = price * subjectFeePercent / 1 ether; + require(msg.value >= price + protocolFee + subjectFee, "Insufficient payment"); + sharesBalance[sharesSubject][msg.sender] = sharesBalance[sharesSubject][msg.sender] + amount; + sharesSupply[sharesSubject] = supply + amount; + emit Trade(msg.sender, sharesSubject, true, amount, price, protocolFee, subjectFee, supply + amount); + (bool success1,) = protocolFeeDestination.call{value: protocolFee}(""); + (bool success2,) = sharesSubject.call{value: subjectFee}(""); + + require(success1 && success2, "Unable to send funds"); + } + + function sellShares(address sharesSubject, uint256 amount) public payable { + uint256 supply = sharesSupply[sharesSubject]; + require(supply > amount, "Cannot sell the last share"); + uint256 price = getPrice(supply - amount, amount); + uint256 protocolFee = price * protocolFeePercent / 1 ether; + uint256 subjectFee = price * subjectFeePercent / 1 ether; + require(sharesBalance[sharesSubject][msg.sender] >= amount, "Insufficient shares"); + sharesBalance[sharesSubject][msg.sender] = sharesBalance[sharesSubject][msg.sender] - amount; + sharesSupply[sharesSubject] = supply - amount; + emit Trade(msg.sender, sharesSubject, false, amount, price, protocolFee, subjectFee, supply - amount); + (bool success1,) = msg.sender.call{value: price - protocolFee - subjectFee}(""); + (bool success2,) = protocolFeeDestination.call{value: protocolFee}(""); + (bool success3,) = sharesSubject.call{value: subjectFee}(""); + + require(success1 && success2 && success3, "Unable to send funds"); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index e9b9e6a..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console2} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/FriendlyAgentTest.t.sol b/test/FriendlyAgentTest.t.sol new file mode 100644 index 0000000..3ba88f1 --- /dev/null +++ b/test/FriendlyAgentTest.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +import {Test, console} from "forge-std/Test.sol"; +import {FriendlyAgent, IFriendtechSharesV1} from "../../src/FriendlyAgent.sol"; +import {DeployFriendlyAgent} from "../../script/DeployFriendlyAgent.s.sol"; +import {HelperConfig} from "../../script/HelperConfig.s.sol"; +import {ERC20} from "@openzeppelin-contracts/token/ERC20/ERC20.sol"; + +contract MockToken is ERC20 { + constructor(uint256 initialSupply) ERC20("MockToken", "MT") { + _mint(msg.sender, initialSupply); + } +} + +contract FriendlyAgentTest is Test { + DeployFriendlyAgent deployFriendlyAgent; + FriendlyAgent friendlyAgent; + + uint256 constant STARTING_BALANCE = 10 ether; + address ALICE = makeAddr("alice"); // Owner + address BOB = makeAddr("bob"); // Not Owner + IFriendtechSharesV1 friendtechShares; + HelperConfig helperConfig; + MockToken mockToken; + + function setUp() external { + helperConfig = new HelperConfig(); + + vm.startBroadcast(ALICE); + friendlyAgent = new FriendlyAgent(helperConfig.friendsTechAddress()); + mockToken = new MockToken(100); + vm.stopBroadcast(); + + friendtechShares = friendlyAgent.i_friendtechShares(); + vm.deal(ALICE, STARTING_BALANCE); + vm.deal(BOB, STARTING_BALANCE); + } + + modifier setupShares() { + // Need to buy 1 share to get over arithmetic over/underflow + uint256 priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 1); + + vm.prank(ALICE); + friendtechShares.buyShares{value: priceAfterFee}(ALICE, 1); + + priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 4); + + vm.prank(ALICE); + friendtechShares.buyShares{value: priceAfterFee}(ALICE, 4); + _; + } + + // Testing owner functionality + + function testOwnerCanBuy() public setupShares { + uint256 priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 1); + + vm.prank(ALICE); + friendlyAgent.buyShares{value: priceAfterFee}(priceAfterFee + 10, 1, ALICE); + assert(friendlyAgent.getHoldings(ALICE) == 1); + } + + function testOwnerCantBuyOverLimit() public setupShares { + uint256 priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 1); + + vm.prank(ALICE); + vm.expectRevert(FriendlyAgent.FriendlyAgent__OverMaxLimit.selector); + friendlyAgent.buyShares{value: priceAfterFee}(priceAfterFee - 10, 1, ALICE); + } + + function testOwnerCanSell() public setupShares { + uint256 priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 1); + + vm.prank(ALICE); + friendlyAgent.buyShares{value: priceAfterFee}(priceAfterFee + 10, 1, ALICE); + assert(friendlyAgent.getHoldings(ALICE) == 1); + + priceAfterFee = friendtechShares.getSellPriceAfterFee(ALICE, 1); + assert(address(friendlyAgent).balance == 0); + + vm.prank(ALICE); + friendlyAgent.sellShares(priceAfterFee - 10, 1, ALICE); + assert(address(friendlyAgent).balance != 0); + } + + function testOwnerCantSellUnderLimit() public setupShares { + uint256 priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 1); + + vm.prank(ALICE); + friendlyAgent.buyShares{value: priceAfterFee}(priceAfterFee + 10, 1, ALICE); + assert(friendlyAgent.getHoldings(ALICE) == 1); + + priceAfterFee = friendtechShares.getSellPriceAfterFee(ALICE, 1); + assert(address(friendlyAgent).balance == 0); + + vm.prank(ALICE); + vm.expectRevert(FriendlyAgent.FriendlyAgent__UnderMinLimit.selector); + friendlyAgent.sellShares(priceAfterFee + 10, 1, ALICE); + } + + function testOwnerCanWithdraw() public { + vm.prank(ALICE); + payable(address(friendlyAgent)).transfer(5 ether); + uint256 aliceBalance = ALICE.balance; + + assert(aliceBalance == 5 ether); + + vm.prank(ALICE); + friendlyAgent.withdraw(); + + assert(aliceBalance == 5 ether); + } + + function testOwnerCanWithdrawToken() public { + vm.prank(ALICE); + mockToken.transfer(address(friendlyAgent), 10); + + assert(mockToken.balanceOf(address(friendlyAgent)) == 10); + assert(mockToken.balanceOf(address(ALICE)) == 90); + + vm.prank(ALICE); + friendlyAgent.withdrawToken(address(mockToken)); + + assert(mockToken.balanceOf(address(friendlyAgent)) == 0); + assert(mockToken.balanceOf(ALICE) == 100); + } + + // Test non owners reverts + + function testNonOwnerCantBuy() public setupShares { + uint256 priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 1); + + vm.prank(BOB); + vm.expectRevert(FriendlyAgent.FriendlyAgent__NotOwner.selector); + friendlyAgent.buyShares{value: priceAfterFee}(priceAfterFee + 10, 1, ALICE); + } + + function testOwnerCantSell() public setupShares { + uint256 priceAfterFee = friendtechShares.getBuyPriceAfterFee(ALICE, 1); + + vm.prank(ALICE); + friendlyAgent.buyShares{value: priceAfterFee}(priceAfterFee + 10, 1, ALICE); + assert(friendlyAgent.getHoldings(ALICE) == 1); + + priceAfterFee = friendtechShares.getSellPriceAfterFee(ALICE, 1); + assert(address(friendlyAgent).balance == 0); + + vm.prank(BOB); + vm.expectRevert(FriendlyAgent.FriendlyAgent__NotOwner.selector); + friendlyAgent.sellShares(priceAfterFee - 10, 1, ALICE); + } + + function testNonOwnerCantWithdraw() public setupShares { + vm.prank(BOB); + payable(address(friendlyAgent)).transfer(1 ether); + + vm.prank(BOB); + vm.expectRevert(FriendlyAgent.FriendlyAgent__NotOwner.selector); + friendlyAgent.withdraw(); + } + + function testNonOwnerCantWithdrawToken() public { + vm.prank(ALICE); + mockToken.transfer(address(friendlyAgent), 10); + + assert(mockToken.balanceOf(address(friendlyAgent)) == 10); + + vm.prank(BOB); + vm.expectRevert(FriendlyAgent.FriendlyAgent__NotOwner.selector); + friendlyAgent.withdrawToken(address(mockToken)); + } +}