diff --git a/.env.example b/.env.example index 309cc43..682e216 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,8 @@ SEPOLIA_DEPLOYER_PK= OPTIMISM_SEPOLIA_RPC=https://opt-sepolia.g.alchemy.com/v2/YNob1yS6fZux6Fs44VAybG3JXP4k8QgN OPTIMISM_SEPOLIA_DEPLOYER_PK= +VIRTUAL_OPTIMISM_RPC= +TENDERLY_ACCESS_KEY= +TENDERLY_VERIFIER_URL= + ETHERSCAN_API_KEY=ZIGN5Y8QXYQ5SH17D4RA2YQWNM2DP62S89 diff --git a/foundry.toml b/foundry.toml index e7a0924..fe1ee65 100644 --- a/foundry.toml +++ b/foundry.toml @@ -36,3 +36,5 @@ optimism-sepolia = "${OPTIMISM_SEPOLIA_RPC}" mainnet = { key = "${ETHERSCAN_API_KEY}", chain = "mainnet" } sepolia = { key = "${ETHERSCAN_API_KEY}", chain = "sepolia" } optimism-sepolia = { key = "${ETHERSCAN_API_KEY}", chain = "optimism-sepolia" } +arbitrum-sepolia = { key = "${ARBITRUM_ETHERSCAN_API_KEY}", chain = "arbitrum-sepolia" } +unknown_chain = { key = "${TENDERLY_ACCESS_KEY}", chain = 4924, url = "${VIRTUAL_OPTIMISM_RPC}/verify/etherscan" } diff --git a/package.json b/package.json index 96b3659..3602fa7 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "coverage": "forge coverage --report summary --report lcov --match-path 'test/unit/*'", + "deploy:arbitrum-sepolia": "bash -c 'source .env && forge script Deploy -vvvvv --rpc-url $ARBITRUM_SEPOLIA_RPC --broadcast --chain arbitrum-sepolia --private-key $ARBITRUM_SEPOLIA_DEPLOYER_PK --verify --verifier blockscout --verifier-url https://arbitrum-sepolia.blockscout.com/api/'", "deploy:mainnet": "bash -c 'source .env && forge script Deploy -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK'", "deploy:optimism-sepolia": "bash -c 'source .env && forge script Deploy -vvvvv --rpc-url $OPTIMISM_SEPOLIA_RPC --broadcast --chain optimism-sepolia --private-key $OPTIMISM_SEPOLIA_DEPLOYER_PK --verify --verifier blockscout --verifier-url https://optimism-sepolia.blockscout.com/api/'", "deploy:sepolia": "bash -c 'source .env && forge script Deploy -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK'", + "deploy:v-optimism": "bash -c 'source .env && forge script Deploy -vvvvv --rpc-url $VIRTUAL_OPTIMISM_RPC --broadcast --private-key $OPTIMISM_DEPLOYER_PK --verify'", "lint:check": "yarn lint:sol-tests && yarn lint:sol-logic && forge fmt --check", "lint:fix": "sort-package-json && forge fmt && yarn lint:sol-tests --fix && yarn lint:sol-logic --fix", "lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js", diff --git a/script/Deploy.sol b/script/Deploy.sol index 0094792..dc31f6f 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -20,11 +20,28 @@ contract Deploy is Script { address[] memory _tokens = new address[](1); _tokens[0] = address(0x5fd84259d66Cd46123540766Be93DFE6D43130D7); + address[] memory _tokensOptimismSepolia = new address[](2); + _tokensOptimismSepolia[0] = address(0x7F5c764cBc14f9669B88837ca1490cCa17c31607); + _tokensOptimismSepolia[1] = address(0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1); + + address[] memory _tokensArbitrumSepolia = new address[](1); + _tokensArbitrumSepolia[0] = address( + 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d // usdc + ); + // Mainnet _deploymentParams[1] = DeploymentParams(_tokens, IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2)); // Optimism Sepolia _deploymentParams[11_155_420] = DeploymentParams(_tokens, IPool(0xb50201558B00496A145fE76f7424749556E326D8)); + + // V-Optimism + _deploymentParams[4924] = + DeploymentParams(_tokensOptimismSepolia, IPool(0x794a61358D6845594F94dc1DB02A252b5b4814aD)); + + // Arbitrum + _deploymentParams[421_614] = + DeploymentParams(_tokensArbitrumSepolia, IPool(0xBfC91D59fdAA134A4ED45f7B584cAf96D7792Eff)); } function run() public { @@ -34,10 +51,10 @@ contract Deploy is Script { Grateful _grateful = new Grateful(_params.tokens, _params.aavePool); AaveV3Vault _vault = new AaveV3Vault( ERC20(_params.tokens[0]), - ERC20(0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c), + ERC20(0x460b97BD498E1157530AEb3086301d5225b91216), _params.aavePool, address(0), - IRewardsController(0x8164Cc65827dcFe994AB23944CBC90e0aa80bFcb), + IRewardsController(0x3A203B14CF8749a1e3b7314c6c49004B77Ee667A), address(_grateful) ); _grateful.addVault(_params.tokens[0], address(_vault)); diff --git a/src/contracts/Grateful.sol b/src/contracts/Grateful.sol index 4d17fa0..74fcbb9 100644 --- a/src/contracts/Grateful.sol +++ b/src/contracts/Grateful.sol @@ -36,6 +36,9 @@ contract Grateful is IGrateful, Ownable2Step { // @inheritdoc IGrateful uint256 public subscriptionCount; + // @inheritdoc IGrateful + uint256 public fee; + modifier onlyWhenTokenWhitelisted(address _token) { if (!tokensWhitelisted[_token]) { revert Grateful_TokenNotWhitelisted(); @@ -205,6 +208,11 @@ contract Grateful is IGrateful, Ownable2Step { yieldingFunds[msg.sender] = !yieldingFunds[msg.sender]; } + function applyFee(uint256 amount) public view returns (uint256) { + uint256 fee = (amount * 100) / 10_000; + return amount - fee; + } + /** * @notice Processes a payment * @param _sender Address of the sender @@ -222,20 +230,23 @@ contract Grateful is IGrateful, Ownable2Step { uint256 _paymentId, uint256 _subscriptionId ) internal { + uint256 amountWithFee = applyFee(_amount); if (yieldingFunds[_merchant]) { AaveV3ERC4626 vault = vaults[_token]; if (address(vault) == address(0)) { revert Grateful_VaultNotSet(); } - IERC20(_token).transferFrom(_sender, address(this), _amount); - uint256 _shares = vault.deposit(_amount, address(this)); + IERC20(_token).transferFrom(_sender, address(this), amountWithFee); + uint256 _shares = vault.deposit(amountWithFee, address(this)); shares[_merchant][_token] += _shares; } else { - if (!IERC20(_token).transferFrom(_sender, _merchant, _amount)) { + if (!IERC20(_token).transferFrom(_sender, _merchant, amountWithFee)) { revert Grateful_TransferFailed(); } } + IERC20(_token).transferFrom(_sender, owner(), _amount - amountWithFee); + emit PaymentProcessed(_sender, _merchant, _token, _amount, yieldingFunds[_merchant], _paymentId, _subscriptionId); } } diff --git a/src/interfaces/IGrateful.sol b/src/interfaces/IGrateful.sol index e7fd152..8108746 100644 --- a/src/interfaces/IGrateful.sol +++ b/src/interfaces/IGrateful.sol @@ -136,6 +136,12 @@ interface IGrateful { */ function subscriptionCount() external view returns (uint256 _subscriptionCount); + /** + * @notice Returns the fee applied to the payments + * @return _fee Fee applied to the payments + */ + function fee() external view returns (uint256); + /*/////////////////////////////////////////////////////////////// LOGIC //////////////////////////////////////////////////////////////*/ @@ -250,4 +256,9 @@ interface IGrateful { address _token, uint256 _amount ) external view returns (uint256); + + /// @notice Applies the fee to an amount + /// @param amount Amount of the token + /// @return amountWithFee Amount of the token with the fee applied + function applyFee(uint256 amount) external view returns (uint256); } diff --git a/test/integration/Grateful.t.sol b/test/integration/Grateful.t.sol index 2b2152d..43b655c 100644 --- a/test/integration/Grateful.t.sol +++ b/test/integration/Grateful.t.sol @@ -12,7 +12,7 @@ contract IntegrationGreeter is IntegrationBase { ); vm.stopPrank(); - assertEq(_usdc.balanceOf(_merchant), _amount); + assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount)); } function test_PaymentYieldingFunds() public { @@ -35,7 +35,7 @@ contract IntegrationGreeter is IntegrationBase { vm.prank(_merchant); _grateful.withdraw(address(_usdc)); - assertGt(_usdc.balanceOf(_merchant), _amount); + assertGt(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount)); } function test_Subscription() public { @@ -45,7 +45,7 @@ contract IntegrationGreeter is IntegrationBase { vm.stopPrank(); // When subscription is created, a initial payment is made - assertEq(_usdc.balanceOf(_merchant), _amount); + assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount)); // Shouldn't be able to process the subscription before 30 days have passed vm.expectRevert(IGrateful.Grateful_TooEarlyForNextPayment.selector); @@ -56,7 +56,7 @@ contract IntegrationGreeter is IntegrationBase { _grateful.processSubscription(subscriptionId); - assertEq(_usdc.balanceOf(_merchant), _amount * 2); + assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount) * 2); // Should revert if the payments amount has been reached @@ -83,6 +83,6 @@ contract IntegrationGreeter is IntegrationBase { _grateful.createOneTimePayment(_merchant, address(_usdc), _amount, 4, paymentId, precomputed); // Merchant receives the payment - assertEq(_usdc.balanceOf(_merchant), _amount); + assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount)); } }