diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G3.sol b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G3.sol index 349f85399..a896a8258 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G3.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_G3.sol @@ -15,6 +15,15 @@ contract LenderCommitmentForwarder_G3 is LenderCommitmentForwarder_G2(_tellerV2, _marketRegistry) {} + /** + * @notice Returns the TellerV2 address for protocol owner checks. + * @dev Implements the abstract function from ExtensionsContextUpgradeable. + * @dev Uses the immutable _tellerV2 from TellerV2MarketForwarder_G2 parent contract. + */ + function _getTellerV2() internal view override returns (address) { + return _tellerV2; + } + function _msgSender() internal view diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol index 0b4f581ae..dd97e1fc1 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/LenderCommitmentForwarder_U1.sol @@ -182,6 +182,15 @@ contract LenderCommitmentForwarder_U1 is UNISWAP_V3_FACTORY = _uniswapV3Factory; } + /** + * @notice Returns the TellerV2 address for protocol owner checks. + * @dev Implements the abstract function from ExtensionsContextUpgradeable. + * @dev Uses the immutable _tellerV2 from TellerV2MarketForwarder_G2 parent contract. + */ + function _getTellerV2() internal view override returns (address) { + return _tellerV2; + } + /** * @notice Creates a loan commitment from a lender for a market. * @param _commitment The new commitment data expressed as a struct diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol b/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol index 2708f081f..cbc639b72 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol @@ -93,11 +93,19 @@ contract SmartCommitmentForwarder is TellerV2MarketForwarder_G3(_protocolAddress, _marketRegistry) { } - function initialize() public initializer { + function initialize() public initializer { __Pausable_init(); __Ownable_init_unchained(); } + /** + * @notice Returns the TellerV2 address for protocol owner checks. + * @dev Implements the abstract function from ExtensionsContextUpgradeable. + * @dev Uses the immutable _tellerV2 from TellerV2MarketForwarder_G2 parent contract. + */ + function _getTellerV2() internal view override returns (address) { + return _tellerV2; + } function setLiquidationProtocolFeePercent(uint256 _percent) public onlyProtocolOwner { diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable.sol index b4d86e4a0..8ba4da9b5 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable.sol @@ -5,23 +5,47 @@ import "../../interfaces/IExtensionsContext.sol"; import "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + abstract contract ExtensionsContextUpgradeable is IExtensionsContext { using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; // Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private userExtensions; + mapping(address => bool) private globalExtensions; + + + event ExtensionAdded(address extension, address sender); event ExtensionRevoked(address extension, address sender); + event GlobalExtensionAdded(address extension, address sender); + event GlobalExtensionRevoked(address extension, address sender); + + /** + * @notice Returns the TellerV2 address used for protocol owner checks. + * @dev Must be implemented by inheriting contracts. + */ + function _getTellerV2() internal view virtual returns (address); + + modifier onlyExtensionsProtocolOwner() { + require( Ownable( _getTellerV2() ).owner() == _msgSender() , "Sender not authorized"); + _; + } + + + function hasExtension(address account, address extension) public view returns (bool) { - return userExtensions[account][extension]; + return userExtensions[account][extension] || globalExtensions[extension] ; } + // ----- + function addExtension(address extension) external { require( _msgSender() != extension, @@ -37,6 +61,22 @@ abstract contract ExtensionsContextUpgradeable is IExtensionsContext { emit ExtensionRevoked(extension, _msgSender()); } + + // ------ + + function addGlobalExtension(address extension) external onlyExtensionsProtocolOwner { + + globalExtensions [extension] = true; + emit GlobalExtensionAdded(extension, _msgSender()); + } + + function revokeGlobalExtension(address extension) external onlyExtensionsProtocolOwner { + globalExtensions[extension] = false; + emit GlobalExtensionRevoked(extension, _msgSender()); + } + + // ------ + function _msgSender() internal view virtual returns (address sender) { address sender; @@ -58,5 +98,5 @@ abstract contract ExtensionsContextUpgradeable is IExtensionsContext { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[49] private __gap; + uint256[48] private __gap; } diff --git a/packages/contracts/contracts/TellerV2Context.sol b/packages/contracts/contracts/TellerV2Context.sol index 5ebb607d5..bc1b227b2 100644 --- a/packages/contracts/contracts/TellerV2Context.sol +++ b/packages/contracts/contracts/TellerV2Context.sol @@ -3,13 +3,20 @@ pragma solidity >=0.8.0 <0.9.0; import "./TellerV2Storage.sol"; import "./ERC2771ContextUpgradeable.sol"; + +import "./interfaces/IOwnable.sol"; + + + /** * @dev This contract should not use any storage + * + * @dev for OnlyProtocolOwner to work, the wrapping contract must implement owner() [ OZ Ownable ] */ abstract contract TellerV2Context is - ERC2771ContextUpgradeable, + ERC2771ContextUpgradeable, TellerV2Storage { using EnumerableSet for EnumerableSet.AddressSet; @@ -19,6 +26,14 @@ abstract contract TellerV2Context is address forwarder, address sender ); + + event ProtocolTrustedForwarderSet( + + address forwarder, + address sender, + bool trusted + ); + event MarketForwarderApproved( uint256 indexed marketId, address indexed forwarder, @@ -34,6 +49,22 @@ abstract contract TellerV2Context is ERC2771ContextUpgradeable(trustedForwarder) {} + + + modifier onlyProtocolOwner() { + require( _owner() == _msgSender() , "Sender not authorized"); + _; + } + + + function _owner() internal returns (address) { + + return IOwnable( address(this) ) .owner() ; + } + + + + /** * @notice Checks if an address is a trusted forwarder contract for a given market. * @param _marketId An ID for a lending market. @@ -45,10 +76,19 @@ abstract contract TellerV2Context is address _trustedMarketForwarder ) public view returns (bool) { return - _trustedMarketForwarders[_marketId] == _trustedMarketForwarder || - lenderCommitmentForwarder == _trustedMarketForwarder; + _trustedMarketForwarders[_marketId] == _trustedMarketForwarder ; } + + function isProtocolTrustedForwarder( + address _forwarder + ) public view returns (bool) { + return + _protocolTrustedForwarders[_forwarder] == true ; + } + + + /** * @notice Checks if an account has approved a forwarder for a market. * @param _marketId An ID for a lending market. @@ -62,10 +102,23 @@ abstract contract TellerV2Context is address _account ) public view returns (bool) { return - isTrustedMarketForwarder(_marketId, _forwarder) && - _approvedForwarderSenders[_forwarder].contains(_account); + ( isTrustedMarketForwarder(_marketId, _forwarder) && + _approvedForwarderSenders[_forwarder].contains(_account) ) + + || isProtocolTrustedForwarder( _forwarder ) + ; } + function setProtocolTrustedForwarder( address _forwarder, bool _trusted) + external onlyProtocolOwner + { + + _protocolTrustedForwarders[_forwarder] = _trusted; + emit ProtocolTrustedForwarderSet( _forwarder, _msgSender(), _trusted); + } + + + /** * @notice Sets a trusted forwarder for a lending market. * @notice The caller must owner the market given. See {MarketRegistry} @@ -161,4 +214,10 @@ abstract contract TellerV2Context is return _msgData(); } } + + + + + + } diff --git a/packages/contracts/contracts/TellerV2Storage.sol b/packages/contracts/contracts/TellerV2Storage.sol index 371e567c6..5dfb89dd6 100644 --- a/packages/contracts/contracts/TellerV2Storage.sol +++ b/packages/contracts/contracts/TellerV2Storage.sol @@ -139,7 +139,7 @@ abstract contract TellerV2Storage_G1 is TellerV2Storage_G0 { } abstract contract TellerV2Storage_G2 is TellerV2Storage_G1 { - address public lenderCommitmentForwarder; + address public lenderCommitmentForwarder; // deprecated } abstract contract TellerV2Storage_G3 is TellerV2Storage_G2 { @@ -171,4 +171,8 @@ abstract contract TellerV2Storage_G8 is TellerV2Storage_G7 { address protocolFeeRecipient; } -abstract contract TellerV2Storage is TellerV2Storage_G8 {} +abstract contract TellerV2Storage_G9 is TellerV2Storage_G8 { + mapping(address => bool) public _protocolTrustedForwarders;} + + +abstract contract TellerV2Storage is TellerV2Storage_G9 {} diff --git a/packages/contracts/contracts/interfaces/IOwnable.sol b/packages/contracts/contracts/interfaces/IOwnable.sol new file mode 100644 index 000000000..d2c954f74 --- /dev/null +++ b/packages/contracts/contracts/interfaces/IOwnable.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + + +interface IOwnable { + + function owner() external view virtual returns (address) ; + + +} diff --git a/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol b/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol index a401a198e..45e0be594 100644 --- a/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol +++ b/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol @@ -1,10 +1,7 @@ pragma solidity >=0.8.0 <0.9.0; // SPDX-License-Identifier: MIT - -import "forge-std/console.sol"; - import {IUniswapPricingLibrary} from "../interfaces/IUniswapPricingLibrary.sol"; diff --git a/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol b/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol index 2982346ae..53b39ab0f 100644 --- a/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol +++ b/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol @@ -1,8 +1,6 @@ pragma solidity >=0.8.0 <0.9.0; // SPDX-License-Identifier: MIT - -import "forge-std/console.sol"; import {IUniswapPricingLibrary} from "../interfaces/IUniswapPricingLibrary.sol"; @@ -52,9 +50,7 @@ contract UniswapPricingHelper ); - console.log("ratio"); - console.logUint(pool0PriceRatio); - console.logUint(pool1PriceRatio); + return FullMath.mulDiv( diff --git a/packages/contracts/deploy/upgrades/35_upgrade_teller_v2_upgraded_context.ts b/packages/contracts/deploy/upgrades/35_upgrade_teller_v2_upgraded_context.ts new file mode 100644 index 000000000..32b49cc1e --- /dev/null +++ b/packages/contracts/deploy/upgrades/35_upgrade_teller_v2_upgraded_context.ts @@ -0,0 +1,62 @@ +import { DeployFunction } from 'hardhat-deploy/dist/types' + +const deployFn: DeployFunction = async (hre) => { + hre.log('----------') + hre.log('') + hre.log('TellerV2: Proposing upgrade...') + + const tellerV2 = await hre.contracts.get('TellerV2') + const metaForwarder = await hre.contracts.get('MetaForwarder') + const v2Calculations = await hre.deployments.get('V2Calculations') + + + + await hre.upgrades.proposeBatchTimelock({ + title: 'TellerV2: Protocol Trusted Forwarder ', + description: ` +# TellerV2 + +* Adds support for improved trusted forwarder. +`, + _steps: [ + { + proxy: tellerV2, + implFactory: await hre.ethers.getContractFactory('TellerV2', { + libraries: { + V2Calculations: v2Calculations.address, + }, + }), + + opts: { + unsafeSkipStorageCheck: true, + unsafeAllow: [ + 'constructor', + 'state-variable-immutable', + 'external-library-linking', + ], + constructorArgs: [await metaForwarder.getAddress()], + }, + }, + ], + }) + + hre.log('done.') + hre.log('') + hre.log('----------') + + return true +} + +// tags and deployment +deployFn.id = 'teller-v2:context-forwarding-upgrade' +deployFn.tags = [ + 'proposal', + 'upgrade', + 'teller-v2', + 'teller-v2:context-forwarding-upgrade', +] +deployFn.dependencies = ['teller-v2:deploy'] +deployFn.skip = async (hre) => { + return !hre.network.live || !['goerli', 'polygon'].includes(hre.network.name) +} +export default deployFn diff --git a/packages/contracts/deploy/upgrades/36_upgrade_smart_commitment_forwarder_context.ts b/packages/contracts/deploy/upgrades/36_upgrade_smart_commitment_forwarder_context.ts new file mode 100644 index 000000000..b1b7d9017 --- /dev/null +++ b/packages/contracts/deploy/upgrades/36_upgrade_smart_commitment_forwarder_context.ts @@ -0,0 +1,59 @@ +import { DeployFunction } from 'hardhat-deploy/dist/types' + +const deployFn: DeployFunction = async (hre) => { + hre.log('----------') + hre.log('') + hre.log('Smart Commitment Forwarder: Proposing upgrade...') + + const tellerV2 = await hre.contracts.get('TellerV2') + const marketRegistry = await hre.contracts.get('MarketRegistry') + + + + const smartCommitmentForwarder = await hre.contracts.get( + 'SmartCommitmentForwarder' + ) + + await hre.upgrades.proposeBatchTimelock({ + title: 'Smart Commitment Forwarder: Upgrade', + description: ` +# Smart Commitment Forwarder +* Modifies Smart Commitment Forwarder to upgrade the context forwarder. +`, + _steps: [ + { + proxy: smartCommitmentForwarder, + implFactory: await hre.ethers.getContractFactory( + 'SmartCommitmentForwarder' + ), + + opts: { + unsafeAllow: ['constructor', 'state-variable-immutable'], + unsafeAllowRenames: true, + // unsafeSkipStorageCheck: true, //caution ! + constructorArgs: [ + await tellerV2.getAddress(), + await marketRegistry.getAddress(), + ] + + }, + }, + ], + }) + + hre.log('done.') + hre.log('') + hre.log('----------') + + return true +} + +// tags and deployment +deployFn.id = 'smart-commitment-forwarder:upgrade-context-forwarding' +deployFn.tags = ['proposal', 'upgrade', 'smart-commitment-forwarder-upgrade-context-forwarding'] +deployFn.dependencies = ['smart-commitment-forwarder:deploy'] +deployFn.skip = async (hre) => { + // return true // ALWAYS SKIP FOR NOW + return !hre.network.live || !['goerli','polygon' ].includes(hre.network.name) +} +export default deployFn diff --git a/packages/contracts/remappings.txt b/packages/contracts/remappings.txt new file mode 100644 index 000000000..7eaaf2afd --- /dev/null +++ b/packages/contracts/remappings.txt @@ -0,0 +1,3 @@ +forge-std/=lib/forge-std/src/ +@openzeppelin/=node_modules/@openzeppelin/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ \ No newline at end of file diff --git a/packages/contracts/tests/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable_Test.sol b/packages/contracts/tests/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable_Test.sol index 595184f2b..c38def1cc 100644 --- a/packages/contracts/tests/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable_Test.sol +++ b/packages/contracts/tests/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable_Test.sol @@ -1,44 +1,215 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + import { Testable } from "../../Testable.sol"; import { ExtensionsContextUpgradeable } from "../../../contracts/LenderCommitmentForwarder/extensions/ExtensionsContextUpgradeable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract ExtensionsContextMock is ExtensionsContextUpgradeable { + address private mockTellerV2; -contract ExtensionsContextMock is ExtensionsContextUpgradeable {} + constructor(address _tellerV2) { + mockTellerV2 = _tellerV2; + } + + function _getTellerV2() internal view override returns (address) { + return mockTellerV2; + } +} + +contract MockTellerV2 is Ownable { + constructor(address owner) { + _transferOwnership(owner); + } +} contract ExtensionsContext_Test is Testable { constructor() {} - User private extensionContract; + address private extensionContract; + address private protocolOwner; User private borrower; User private lender; ExtensionsContextMock extensionsContext; + MockTellerV2 mockTellerV2; function setUp() public { borrower = new User(); lender = new User(); + extensionContract = address(new User()); + protocolOwner = address(this); - extensionsContext = new ExtensionsContextMock(); + // Deploy mock TellerV2 with this contract as owner + mockTellerV2 = new MockTellerV2(protocolOwner); + + // Deploy ExtensionsContext with mock TellerV2 + extensionsContext = new ExtensionsContextMock(address(mockTellerV2)); } function test_addingExtension() public { bool isTrustedBefore = extensionsContext.hasExtension( address(borrower), - address(extensionContract) + extensionContract ); //the user will approve vm.prank(address(borrower)); - extensionsContext.addExtension(address(extensionContract)); + extensionsContext.addExtension(extensionContract); + + bool isTrustedAfter = extensionsContext.hasExtension( + address(borrower), + extensionContract + ); + assertFalse(isTrustedBefore, "Should not be trusted extension before"); + assertTrue(isTrustedAfter, "Should be trusted extension after"); + } + + function test_revokingExtension() public { + // First add the extension vm.prank(address(borrower)); + extensionsContext.addExtension(extensionContract); + + // Verify it's added + assertTrue( + extensionsContext.hasExtension(address(borrower), extensionContract), + "Extension should be added" + ); + + // Revoke the extension + vm.prank(address(borrower)); + extensionsContext.revokeExtension(extensionContract); + + // Verify it's revoked + assertFalse( + extensionsContext.hasExtension(address(borrower), extensionContract), + "Extension should be revoked" + ); + } + + function test_hasExtension_false_for_different_user() public { + // Borrower adds extension + vm.prank(address(borrower)); + extensionsContext.addExtension(extensionContract); + + // Should not be approved for lender + assertFalse( + extensionsContext.hasExtension(address(lender), extensionContract), + "Extension should not be approved for different user" + ); + } + + function test_addGlobalExtension_by_protocol_owner() public { + bool isTrustedBefore = extensionsContext.hasExtension( + address(borrower), + extensionContract + ); + + // Protocol owner adds global extension + extensionsContext.addGlobalExtension(extensionContract); + bool isTrustedAfter = extensionsContext.hasExtension( address(borrower), - address(extensionContract) + extensionContract ); - assertFalse(isTrustedBefore, "Should not be trusted forwarder before"); - assertTrue(isTrustedAfter, "Should be trusted forwarder after"); + assertFalse(isTrustedBefore, "Should not be trusted extension before"); + assertTrue(isTrustedAfter, "Should be trusted extension after being added as global"); + } + + function test_globalExtension_works_for_all_users() public { + // Protocol owner adds global extension + extensionsContext.addGlobalExtension(extensionContract); + + // Should work for borrower + assertTrue( + extensionsContext.hasExtension(address(borrower), extensionContract), + "Global extension should work for borrower" + ); + + // Should work for lender + assertTrue( + extensionsContext.hasExtension(address(lender), extensionContract), + "Global extension should work for lender" + ); + + // Should work for any random address + assertTrue( + extensionsContext.hasExtension(address(123), extensionContract), + "Global extension should work for any address" + ); + } + + function test_revokeGlobalExtension_by_protocol_owner() public { + // Add global extension + extensionsContext.addGlobalExtension(extensionContract); + + assertTrue( + extensionsContext.hasExtension(address(borrower), extensionContract), + "Extension should be trusted after adding as global" + ); + + // Revoke global extension + extensionsContext.revokeGlobalExtension(extensionContract); + + assertFalse( + extensionsContext.hasExtension(address(borrower), extensionContract), + "Extension should not be trusted after revoking global status" + ); + } + + function test_globalExtension_overrides_lack_of_user_approval() public { + // Add as global extension WITHOUT user approval + extensionsContext.addGlobalExtension(extensionContract); + + // Should still return true even though user never approved + assertTrue( + extensionsContext.hasExtension(address(borrower), extensionContract), + "Global extension should work without user approval" + ); + } + + function test_userExtension_still_works_after_globalExtension_revoked() public { + // User adds extension + vm.prank(address(borrower)); + extensionsContext.addExtension(extensionContract); + + // Protocol owner adds as global + extensionsContext.addGlobalExtension(extensionContract); + + // Protocol owner revokes global status + extensionsContext.revokeGlobalExtension(extensionContract); + + // Should still work for borrower due to user-level approval + assertTrue( + extensionsContext.hasExtension(address(borrower), extensionContract), + "User extension should still work after global is revoked" + ); + + // Should NOT work for lender who never approved + assertFalse( + extensionsContext.hasExtension(address(lender), extensionContract), + "Should not work for user without approval after global is revoked" + ); + } + + function test_addGlobalExtension_reverts_for_non_owner() public { + vm.prank(address(borrower)); + vm.expectRevert("Sender not authorized"); + extensionsContext.addGlobalExtension(extensionContract); + } + + function test_revokeGlobalExtension_reverts_for_non_owner() public { + // First add as global (as owner) + extensionsContext.addGlobalExtension(extensionContract); + + // Try to revoke as non-owner + vm.prank(address(borrower)); + vm.expectRevert("Sender not authorized"); + extensionsContext.revokeGlobalExtension(extensionContract); } } diff --git a/packages/contracts/tests/TellerV2Context/TellerV2Context_Override.sol b/packages/contracts/tests/TellerV2Context/TellerV2Context_Override.sol index bc3084255..51ff843ea 100644 --- a/packages/contracts/tests/TellerV2Context/TellerV2Context_Override.sol +++ b/packages/contracts/tests/TellerV2Context/TellerV2Context_Override.sol @@ -9,11 +9,14 @@ import { IMarketRegistry } from "../../contracts/interfaces/IMarketRegistry.sol" contract TellerV2Context_Override is TellerV2Context { using EnumerableSet for EnumerableSet.AddressSet; + address private _mockOwner; + constructor(address _marketRegistry, address _lenderCommitmentForwarder) TellerV2Context(address(0)) { marketRegistry = IMarketRegistry(_marketRegistry); lenderCommitmentForwarder = _lenderCommitmentForwarder; + _mockOwner = msg.sender; // Set deployer as default owner } function mock_setTrustedMarketForwarder( @@ -32,6 +35,21 @@ contract TellerV2Context_Override is TellerV2Context { _approvedForwarderSenders[_forwarder].add(_account); } + function mock_setProtocolTrustedForwarder( + address _forwarder, + bool _trusted + ) public { + _protocolTrustedForwarders[_forwarder] = _trusted; + } + + function mock_setOwner(address _newOwner) public { + _mockOwner = _newOwner; + } + + function owner() public view returns (address) { + return _mockOwner; + } + function external__msgSenderForMarket(uint256 _marketId) public view diff --git a/packages/contracts/tests/TellerV2Context/TellerV2Context_hasApprovedMarketForwarder.sol b/packages/contracts/tests/TellerV2Context/TellerV2Context_hasApprovedMarketForwarder.sol index bda028ed1..fe12a6d21 100644 --- a/packages/contracts/tests/TellerV2Context/TellerV2Context_hasApprovedMarketForwarder.sol +++ b/packages/contracts/tests/TellerV2Context/TellerV2Context_hasApprovedMarketForwarder.sol @@ -75,4 +75,126 @@ contract TellerV2Context_hasApprovedMarketForwarder is Testable { "forwarder should be approved" ); } + + function test_True_for_protocol_trusted_forwarder_without_market_trust() + public + { + uint256 marketId = 7; + address protocolForwarder = address(456); + + // Set as protocol trusted forwarder (but NOT market trusted) + context.mock_setProtocolTrustedForwarder(protocolForwarder, true); + + // Should return true even without market-specific trust or approval + assertTrue( + context.hasApprovedMarketForwarder( + marketId, + protocolForwarder, + address(this) + ), + "protocol trusted forwarder should be approved for any market" + ); + } + + function test_True_for_protocol_trusted_forwarder_without_account_approval() + public + { + uint256 marketId = 7; + address protocolForwarder = address(456); + address randomAccount = address(789); + + // Set as protocol trusted forwarder + context.mock_setProtocolTrustedForwarder(protocolForwarder, true); + + // Should return true even without explicit account approval + assertTrue( + context.hasApprovedMarketForwarder( + marketId, + protocolForwarder, + randomAccount + ), + "protocol trusted forwarder should bypass account approval requirement" + ); + } + + function test_True_for_protocol_trusted_forwarder_overrides_market_settings() + public + { + uint256 marketId = 7; + address protocolForwarder = address(456); + address differentMarketForwarder = address(789); + + // Set a different forwarder as trusted for the market + context.mock_setTrustedMarketForwarder( + marketId, + differentMarketForwarder + ); + + // Set another forwarder as protocol trusted + context.mock_setProtocolTrustedForwarder(protocolForwarder, true); + + // Protocol trusted forwarder should still return true + assertTrue( + context.hasApprovedMarketForwarder( + marketId, + protocolForwarder, + address(this) + ), + "protocol trusted forwarder should work regardless of market-specific settings" + ); + } + + function test_False_when_protocol_trusted_forwarder_is_unset() public { + uint256 marketId = 7; + address protocolForwarder = address(456); + + // Set as protocol trusted then unset + context.mock_setProtocolTrustedForwarder(protocolForwarder, true); + context.mock_setProtocolTrustedForwarder(protocolForwarder, false); + + // Should return false after being unset (and no market approval) + assertFalse( + context.hasApprovedMarketForwarder( + marketId, + protocolForwarder, + address(this) + ), + "unset protocol forwarder without market trust should not be approved" + ); + } + + function test_Protocol_trusted_forwarder_works_across_multiple_markets() + public + { + address protocolForwarder = address(456); + + // Set as protocol trusted forwarder + context.mock_setProtocolTrustedForwarder(protocolForwarder, true); + + // Should work for multiple different markets + assertTrue( + context.hasApprovedMarketForwarder( + 1, + protocolForwarder, + address(this) + ), + "should work for market 1" + ); + assertTrue( + context.hasApprovedMarketForwarder( + 2, + protocolForwarder, + address(this) + ), + "should work for market 2" + ); + assertTrue( + context.hasApprovedMarketForwarder( + 999, + protocolForwarder, + address(this) + ), + "should work for market 999" + ); + } } diff --git a/packages/contracts/tests/TellerV2Context/TellerV2Context_isProtocolTrustedForwarder.sol b/packages/contracts/tests/TellerV2Context/TellerV2Context_isProtocolTrustedForwarder.sol new file mode 100644 index 000000000..74fd91c99 --- /dev/null +++ b/packages/contracts/tests/TellerV2Context/TellerV2Context_isProtocolTrustedForwarder.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Testable } from "../Testable.sol"; +import { TellerV2Context_Override, TellerV2Context } from "./TellerV2Context_Override.sol"; + +contract TellerV2Context_isProtocolTrustedForwarder is Testable { + TellerV2Context_Override private context; + + address stubbedForwarder = address(0x123); + + function setUp() public { + context = new TellerV2Context_Override(address(0), address(111)); + } + + function test_Returns_false_for_untrusted_forwarder() public { + assertFalse( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should not be trusted by default" + ); + } + + function test_Returns_false_for_zero_address() public { + assertFalse( + context.isProtocolTrustedForwarder(address(0)), + "Zero address should not be trusted" + ); + } + + function test_Returns_true_for_trusted_forwarder() public { + // Setup: Set forwarder as trusted + context.mock_setProtocolTrustedForwarder(stubbedForwarder, true); + + // Verify + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should be trusted" + ); + } + + function test_Returns_false_after_forwarder_is_untrusted() public { + // Setup: Set forwarder as trusted then untrusted + context.mock_setProtocolTrustedForwarder(stubbedForwarder, true); + context.mock_setProtocolTrustedForwarder(stubbedForwarder, false); + + // Verify + assertFalse( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should not be trusted after being set to false" + ); + } + + function test_Returns_true_after_using_setProtocolTrustedForwarder() + public + { + // Use the actual function (not mock) + context.setProtocolTrustedForwarder(stubbedForwarder, true); + + // Verify + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should be trusted after calling setProtocolTrustedForwarder" + ); + } + + function test_Multiple_forwarders_can_be_trusted_independently() public { + address forwarder1 = address(0x111); + address forwarder2 = address(0x222); + address forwarder3 = address(0x333); + + // Set some forwarders as trusted + context.mock_setProtocolTrustedForwarder(forwarder1, true); + context.mock_setProtocolTrustedForwarder(forwarder3, true); + + // Verify individual states + assertTrue( + context.isProtocolTrustedForwarder(forwarder1), + "Forwarder 1 should be trusted" + ); + assertFalse( + context.isProtocolTrustedForwarder(forwarder2), + "Forwarder 2 should not be trusted" + ); + assertTrue( + context.isProtocolTrustedForwarder(forwarder3), + "Forwarder 3 should be trusted" + ); + } + + function test_State_persists_across_multiple_calls() public { + // Set forwarder as trusted + context.mock_setProtocolTrustedForwarder(stubbedForwarder, true); + + // Call multiple times to verify state persistence + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "First call should return true" + ); + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Second call should return true" + ); + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Third call should return true" + ); + } + + function test_Different_addresses_return_different_states() public { + address trustedForwarder = address(0x111); + address untrustedForwarder = address(0x222); + + // Set only one as trusted + context.mock_setProtocolTrustedForwarder(trustedForwarder, true); + + // Verify they have different states + assertTrue( + context.isProtocolTrustedForwarder(trustedForwarder), + "Trusted forwarder should return true" + ); + assertFalse( + context.isProtocolTrustedForwarder(untrustedForwarder), + "Untrusted forwarder should return false" + ); + } +} diff --git a/packages/contracts/tests/TellerV2Context/TellerV2Context_isTrustedMarketForwarder.sol b/packages/contracts/tests/TellerV2Context/TellerV2Context_isTrustedMarketForwarder.sol index 924aaba9b..0667a2b65 100644 --- a/packages/contracts/tests/TellerV2Context/TellerV2Context_isTrustedMarketForwarder.sol +++ b/packages/contracts/tests/TellerV2Context/TellerV2Context_isTrustedMarketForwarder.sol @@ -15,29 +15,7 @@ contract TellerV2Context_isTrustedMarketForwarder is Testable { lenderCommitmentForwarder ); } - - function test_Lender_Commitment_Forwarder_trusted_by_all_markets() public { - bool isTrusted1 = context.isTrustedMarketForwarder( - 1, - lenderCommitmentForwarder - ); - bool isTrusted7 = context.isTrustedMarketForwarder( - 7, - lenderCommitmentForwarder - ); - bool isTrusted34 = context.isTrustedMarketForwarder( - 34, - lenderCommitmentForwarder - ); - bool isTrusted89 = context.isTrustedMarketForwarder( - 89, - lenderCommitmentForwarder - ); - assertTrue( - isTrusted1 && isTrusted7 && isTrusted34 && isTrusted89, - "lenderCommitmentForwarder should be a trusted forwarder for all markets" - ); - } + function test_Custom_Market_Forwarder_trusted_for_market() public { address stubbedMarketForwarder = address(123); diff --git a/packages/contracts/tests/TellerV2Context/TellerV2Context_setProtocolTrustedForwarder.sol b/packages/contracts/tests/TellerV2Context/TellerV2Context_setProtocolTrustedForwarder.sol new file mode 100644 index 000000000..9db4c68f9 --- /dev/null +++ b/packages/contracts/tests/TellerV2Context/TellerV2Context_setProtocolTrustedForwarder.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Testable } from "../Testable.sol"; +import { TellerV2Context_Override, TellerV2Context } from "./TellerV2Context_Override.sol"; + +contract TellerV2Context_setProtocolTrustedForwarder is Testable { + TellerV2Context_Override private context; + + address protocolOwner = address(this); + address nonOwner = address(0x456); + address stubbedForwarder = address(0x123); + + function setUp() public { + context = new TellerV2Context_Override(address(0), address(111)); + } + + event ProtocolTrustedForwarderSet( + address forwarder, + address sender, + bool trusted + ); + + function test_Successfully_set_protocol_trusted_forwarder_to_true() public { + // Verify initial state + assertFalse( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should not be trusted initially" + ); + + // Expect event emission + vm.expectEmit(true, true, true, true, address(context)); + emit ProtocolTrustedForwarderSet(stubbedForwarder, protocolOwner, true); + + // Set forwarder as trusted + context.setProtocolTrustedForwarder(stubbedForwarder, true); + + // Verify state changed + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should be trusted after setting" + ); + } + + function test_Successfully_set_protocol_trusted_forwarder_to_false() public { + // Setup: Set forwarder as trusted first + context.mock_setProtocolTrustedForwarder(stubbedForwarder, true); + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should be trusted initially" + ); + + // Expect event emission + vm.expectEmit(true, true, true, true, address(context)); + emit ProtocolTrustedForwarderSet(stubbedForwarder, protocolOwner, false); + + // Unset forwarder as trusted + context.setProtocolTrustedForwarder(stubbedForwarder, false); + + // Verify state changed + assertFalse( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Forwarder should not be trusted after unsetting" + ); + } + + function test_Fail_when_non_owner_tries_to_set_protocol_trusted_forwarder() + public + { + // Setup: Change to non-owner context + vm.startPrank(nonOwner); + + // Expect revert + vm.expectRevert("Sender not authorized"); + context.setProtocolTrustedForwarder(stubbedForwarder, true); + + vm.stopPrank(); + } + + function test_Successfully_set_multiple_protocol_trusted_forwarders() + public + { + address forwarder1 = address(0x111); + address forwarder2 = address(0x222); + address forwarder3 = address(0x333); + + // Set multiple forwarders as trusted + context.setProtocolTrustedForwarder(forwarder1, true); + context.setProtocolTrustedForwarder(forwarder2, true); + context.setProtocolTrustedForwarder(forwarder3, true); + + // Verify all are trusted + assertTrue( + context.isProtocolTrustedForwarder(forwarder1), + "Forwarder 1 should be trusted" + ); + assertTrue( + context.isProtocolTrustedForwarder(forwarder2), + "Forwarder 2 should be trusted" + ); + assertTrue( + context.isProtocolTrustedForwarder(forwarder3), + "Forwarder 3 should be trusted" + ); + } + + function test_Successfully_toggle_protocol_trusted_forwarder_state() + public + { + // Set to true + context.setProtocolTrustedForwarder(stubbedForwarder, true); + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Should be trusted after setting to true" + ); + + // Set to false + context.setProtocolTrustedForwarder(stubbedForwarder, false); + assertFalse( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Should not be trusted after setting to false" + ); + + // Set to true again + context.setProtocolTrustedForwarder(stubbedForwarder, true); + assertTrue( + context.isProtocolTrustedForwarder(stubbedForwarder), + "Should be trusted after setting to true again" + ); + } + + function test_Event_emitted_with_correct_parameters_when_setting_trusted() + public + { + vm.expectEmit(true, true, true, true, address(context)); + emit ProtocolTrustedForwarderSet(stubbedForwarder, protocolOwner, true); + + context.setProtocolTrustedForwarder(stubbedForwarder, true); + } + + function test_Event_emitted_with_correct_parameters_when_setting_untrusted() + public + { + vm.expectEmit(true, true, true, true, address(context)); + emit ProtocolTrustedForwarderSet( + stubbedForwarder, + protocolOwner, + false + ); + + context.setProtocolTrustedForwarder(stubbedForwarder, false); + } +} diff --git a/packages/contracts/tests/upgrades/SmartCommitmentForwarder_ContextUpgrade.sol b/packages/contracts/tests/upgrades/SmartCommitmentForwarder_ContextUpgrade.sol new file mode 100644 index 000000000..8487194af --- /dev/null +++ b/packages/contracts/tests/upgrades/SmartCommitmentForwarder_ContextUpgrade.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Testable } from "../Testable.sol"; +import "../../contracts/LenderCommitmentForwarder/SmartCommitmentForwarder.sol"; + +contract SmartCommitmentForwarder_ContextUpgrade_Test is Testable { + + // Polygon SmartCommitmentForwarder proxy address + address constant SMART_COMMITMENT_FORWARDER_MAINNET = 0x80314D77E86d70A67126DA86EC823F5fc018c010; + + SmartCommitmentForwarder smartCommitmentForwarder; + + // Struct to store contract state snapshot + struct StateSnapshot { + uint256 liquidationProtocolFeePercent; + bool paused; + address tellerV2; + address marketRegistry; + uint256 lastUnpausedAt; + } + + function setUp() public { + // Fork Polygon at a recent block + vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); + + smartCommitmentForwarder = SmartCommitmentForwarder(SMART_COMMITMENT_FORWARDER_MAINNET); + } + + function test_storage_preserved_after_etch_upgrade() public { + // Get contract state before the upgrade + StateSnapshot memory stateBefore = _getStateSnapshot(); + + // Get the TellerV2 and MarketRegistry addresses for the constructor + address tellerV2Address = smartCommitmentForwarder.getTellerV2(); + address marketRegistryAddress = smartCommitmentForwarder.getMarketRegistry(); + + // Deploy new SmartCommitmentForwarder implementation + SmartCommitmentForwarder newImplementation = new SmartCommitmentForwarder( + tellerV2Address, + marketRegistryAddress + ); + + // Get the runtime code of the new implementation + bytes memory newCode = address(newImplementation).code; + + // Use vm.etch to replace the code at the proxy address + // Note: This simulates an upgrade by replacing the implementation code + // In a real upgrade, this would be done through the proxy admin + vm.etch(SMART_COMMITMENT_FORWARDER_MAINNET, newCode); + + // Get contract state after the etch + StateSnapshot memory stateAfter = _getStateSnapshot(); + + // Assert all state is unchanged + assertEq( + stateAfter.liquidationProtocolFeePercent, + stateBefore.liquidationProtocolFeePercent, + "Liquidation protocol fee percent changed" + ); + assertEq(stateAfter.paused, stateBefore.paused, "Paused status changed"); + assertEq(stateAfter.tellerV2, stateBefore.tellerV2, "TellerV2 address changed"); + assertEq(stateAfter.marketRegistry, stateBefore.marketRegistry, "MarketRegistry address changed"); + assertEq(stateAfter.lastUnpausedAt, stateBefore.lastUnpausedAt, "Last unpaused timestamp changed"); + } + + function _getStateSnapshot() internal view returns (StateSnapshot memory snapshot) { + snapshot.liquidationProtocolFeePercent = smartCommitmentForwarder.getLiquidationProtocolFeePercent(); + snapshot.paused = smartCommitmentForwarder.paused(); + snapshot.tellerV2 = smartCommitmentForwarder.getTellerV2(); + snapshot.marketRegistry = smartCommitmentForwarder.getMarketRegistry(); + snapshot.lastUnpausedAt = smartCommitmentForwarder.getLastUnpausedAt(); + } +} diff --git a/packages/contracts/tests/upgrades/TellerV2_ContextUpgrade.sol b/packages/contracts/tests/upgrades/TellerV2_ContextUpgrade.sol new file mode 100644 index 000000000..3362156f8 --- /dev/null +++ b/packages/contracts/tests/upgrades/TellerV2_ContextUpgrade.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Testable } from "../Testable.sol"; +import "../../contracts/TellerV2.sol"; +import "../../contracts/TellerV2Storage.sol"; + +contract TellerV2_ContextUpgrade_Test is Testable { + + // Mainnet TellerV2 proxy address + address constant TELLER_V2_MAINNET = 0x00182FdB0B880eE24D428e3Cc39383717677C37e; + + // Bid ID to test + uint256 constant TEST_BID_ID = 400; + + TellerV2 tellerV2; + + // Struct to store bid snapshot + struct BidSnapshot { + address borrower; + address lender; + uint256 marketplaceId; + address lendingToken; + uint256 principal; + uint32 acceptedTimestamp; + uint32 lastRepaidTimestamp; + BidState state; + } + + function setUp() public { + // Fork mainnet at a recent block + vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); + + tellerV2 = TellerV2(TELLER_V2_MAINNET); + } + + function test_storage_preserved_after_etch_upgrade() public { + // Get bid details before the upgrade + BidSnapshot memory bidBefore = _getBidSnapshot(TEST_BID_ID); + + // Get protocol fee recipient before the upgrade + address protocolFeeRecipientBefore = tellerV2.getProtocolFeeRecipient(); + + address metaforwarderAddress = address(0); + + // Deploy new TellerV2 implementation + TellerV2 newImplementation = new TellerV2( metaforwarderAddress ); + + // Get the runtime code of the new implementation + bytes memory newCode = address(newImplementation).code; + + // Use vm.etch to replace the code at the proxy address + // Note: This simulates an upgrade by replacing the implementation code + // In a real upgrade, this would be done through the proxy admin + vm.etch(TELLER_V2_MAINNET, newCode); + + // Get bid details after the etch + BidSnapshot memory bidAfter = _getBidSnapshot(TEST_BID_ID); + + // Get protocol fee recipient after the upgrade + address protocolFeeRecipientAfter = tellerV2.getProtocolFeeRecipient(); + + // Assert all bid details are unchanged + assertEq(bidAfter.borrower, bidBefore.borrower, "Borrower address changed"); + assertEq(bidAfter.lender, bidBefore.lender, "Lender address changed"); + assertEq(bidAfter.marketplaceId, bidBefore.marketplaceId, "Marketplace ID changed"); + assertEq(bidAfter.lendingToken, bidBefore.lendingToken, "Lending token address changed"); + assertEq(bidAfter.principal, bidBefore.principal, "Principal amount changed"); + assertEq(bidAfter.acceptedTimestamp, bidBefore.acceptedTimestamp, "Accepted timestamp changed"); + assertEq(bidAfter.lastRepaidTimestamp, bidBefore.lastRepaidTimestamp, "Last repaid timestamp changed"); + assertEq(uint8(bidAfter.state), uint8(bidBefore.state), "Bid state changed"); + + // Assert protocol fee recipient is unchanged + assertEq(protocolFeeRecipientAfter, protocolFeeRecipientBefore, "Protocol fee recipient changed"); + } + + function _getBidSnapshot(uint256 bidId) internal view returns (BidSnapshot memory snapshot) { + // Get loan summary which provides all the key fields we need to verify + ( + address borrower, + address lender, + uint256 marketId, + address principalTokenAddress, + uint256 principalAmount, + uint32 acceptedTimestamp, + uint32 lastRepaidTimestamp, + BidState bidState + ) = tellerV2.getLoanSummary(bidId); + + snapshot.borrower = borrower; + snapshot.lender = lender; + snapshot.marketplaceId = marketId; + snapshot.lendingToken = principalTokenAddress; + snapshot.principal = principalAmount; + snapshot.acceptedTimestamp = acceptedTimestamp; + snapshot.lastRepaidTimestamp = lastRepaidTimestamp; + snapshot.state = bidState; + } +}