diff --git a/script/DeploySetup.s.sol b/script/DeploySetup.s.sol index df3d4c7..34ba349 100644 --- a/script/DeploySetup.s.sol +++ b/script/DeploySetup.s.sol @@ -19,6 +19,7 @@ import {IPriceFeed} from "../src/interfaces/dependencies/IPriceFeed.sol"; import {IMultiCollateralHintHelpers} from "../src/helpers/interfaces/IMultiCollateralHintHelpers.sol"; import {IMultiTroveGetter} from "../src/helpers/interfaces/IMultiTroveGetter.sol"; import {ISatoshiBORouter} from "../src/helpers/interfaces/ISatoshiBORouter.sol"; +import {IReferralManager} from "../src/helpers/interfaces/IReferralManager.sol"; import {IWETH} from "../src/helpers/interfaces/IWETH.sol"; import {SortedTroves} from "../src/core/SortedTroves.sol"; import {SatoshiCore} from "../src/core/SatoshiCore.sol"; @@ -33,6 +34,7 @@ import {Factory} from "../src/core/Factory.sol"; import {MultiCollateralHintHelpers} from "../src/helpers/MultiCollateralHintHelpers.sol"; import {MultiTroveGetter} from "../src/helpers/MultiTroveGetter.sol"; import {SatoshiBORouter} from "../src/helpers/SatoshiBORouter.sol"; +import {ReferralManager} from "../src/helpers/ReferralManager.sol"; import { SATOSHI_CORE_OWNER, SATOSHI_CORE_GUARDIAN, @@ -42,7 +44,9 @@ import { DEBT_TOKEN_SYMBOL, BO_MIN_NET_DEBT, GAS_COMPENSATION, - WETH_ADDRESS + WETH_ADDRESS, + REFERRAL_START_TIMESTAMP, + REFERRAL_END_TIMESTAMP } from "./DeploySetupConfig.sol"; contract DeploySetupScript is Script { @@ -74,6 +78,7 @@ contract DeploySetupScript is Script { IMultiCollateralHintHelpers hintHelpers; IMultiTroveGetter multiTroveGetter; ISatoshiBORouter satoshiBORouter; + IReferralManager referralManager; /* computed contracts for deployment */ // implementation contracts @@ -245,7 +250,18 @@ contract DeploySetupScript is Script { multiTroveGetter = new MultiTroveGetter(); // SatoshiBORouter - satoshiBORouter = new SatoshiBORouter(debtToken, borrowerOperationsProxy, IWETH(WETH_ADDRESS)); + address cpSatoshiBORouterAddr = vm.computeCreateAddress(deployer, ++nonce); + address cpReferralManagerAddr = vm.computeCreateAddress(deployer, ++nonce); + satoshiBORouter = new SatoshiBORouter( + debtToken, borrowerOperationsProxy, IReferralManager(cpReferralManagerAddr), IWETH(WETH_ADDRESS) + ); + assert(cpSatoshiBORouterAddr == address(satoshiBORouter)); + + // ReferralManager + referralManager = new ReferralManager( + ISatoshiBORouter(cpSatoshiBORouterAddr), REFERRAL_START_TIMESTAMP, REFERRAL_END_TIMESTAMP + ); + assert(cpReferralManagerAddr == address(referralManager)); console.log("Deployed contracts:"); console.log("priceFeedAggregatorImpl:", address(priceFeedAggregatorImpl)); @@ -267,6 +283,7 @@ contract DeploySetupScript is Script { console.log("hintHelpers:", address(hintHelpers)); console.log("multiTroveGetter:", address(multiTroveGetter)); console.log("satoshiBORouter:", address(satoshiBORouter)); + console.log("referralManager:", address(referralManager)); vm.stopBroadcast(); } diff --git a/script/DeploySetupConfig.sol b/script/DeploySetupConfig.sol index 9725529..9a72ea2 100644 --- a/script/DeploySetupConfig.sol +++ b/script/DeploySetupConfig.sol @@ -13,3 +13,7 @@ string constant DEBT_TOKEN_NAME = "Statoshi Stablecoin"; string constant DEBT_TOKEN_SYMBOL = "SAT"; address constant WETH_ADDRESS = 0x51abb19F1ebc7B64040aFd0ef3C789d75C8707e0; + +//TODO: Replace with the actual timestamp +uint256 constant REFERRAL_START_TIMESTAMP = 1709251200; // 2024-03-01 00:00:00 UTC +uint256 constant REFERRAL_END_TIMESTAMP = 1711929600; // 2024-04-01 00:00:00 UTC diff --git a/src/helpers/ReferralManager.sol b/src/helpers/ReferralManager.sol new file mode 100644 index 0000000..f89544e --- /dev/null +++ b/src/helpers/ReferralManager.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IReferralManager} from "./interfaces/IReferralManager.sol"; +import {ISatoshiBORouter} from "./interfaces/ISatoshiBORouter.sol"; + +contract ReferralManager is IReferralManager, Ownable { + ISatoshiBORouter public immutable satoshiBORouter; + + uint256 public startTimestamp; + uint256 public endTimestamp; + + uint256 internal totalPoints; + // borrower => referrer + mapping(address => address) internal referrers; + // referrer => points + mapping(address => uint256) internal points; + + event SetStartTimestamp(uint256 _startTimestamp); + event SetEndTimestamp(uint256 _endTimestamp); + event ExecuteReferral(address indexed borrower, address indexed referrer, uint256 points); + + error InvalidTimestamp(uint256 timestamp); + error InvalidZeroAddress(); + error InvalidSelfReferral(); + error Unauthorized(address _caller); + + modifier onlySatoshiBORouter() { + if (msg.sender != address(satoshiBORouter)) revert Unauthorized(msg.sender); + _; + } + + constructor(ISatoshiBORouter _satoshiBORouter, uint256 _startTimestamp, uint256 _endTimestamp) { + if (address(_satoshiBORouter) == address(0)) revert InvalidZeroAddress(); + + satoshiBORouter = _satoshiBORouter; + startTimestamp = _startTimestamp; + endTimestamp = _endTimestamp; + } + + function executeReferral(address _borrower, address _referrer, uint256 _points) external onlySatoshiBORouter { + // only execute referral if it's active + if (!_isReferralActive()) return; + // no referrer + if (_referrer == address(0)) return; + if (_borrower == _referrer) revert InvalidSelfReferral(); + + address currentReferrer = referrers[_borrower]; + if (currentReferrer == address(0)) { + _setReferrer(_borrower, _referrer); + } else { + // use existing referrer + _referrer = currentReferrer; + } + + _addPoint(_referrer, _points); + _addTotalPoints(_points); + + emit ExecuteReferral(_borrower, _referrer, _points); + } + + function isReferralActive() external view returns (bool) { + return _isReferralActive(); + } + + function _addPoint(address _account, uint256 _points) internal { + points[_account] += _points; + } + + function _addTotalPoints(uint256 _points) internal { + totalPoints += _points; + } + + function _setReferrer(address _account, address _referrer) internal { + referrers[_account] = _referrer; + } + + function _isReferralActive() internal view returns (bool) { + uint256 _timestamp = block.timestamp; + return _timestamp >= startTimestamp && _timestamp <= endTimestamp; + } + + function getTotalPoints() external view returns (uint256) { + return totalPoints; + } + + function getBatchPoints(address[] calldata _accounts) external view returns (uint256[] memory) { + uint256[] memory _points = new uint256[](_accounts.length); + for (uint256 i = 0; i < _accounts.length; i++) { + _points[i] = points[_accounts[i]]; + } + return _points; + } + + function getPoints(address _account) external view returns (uint256) { + return points[_account]; + } + + function getBatchReferrers(address[] calldata _accounts) external view returns (address[] memory) { + address[] memory _referrers = new address[](_accounts.length); + for (uint256 i = 0; i < _accounts.length; i++) { + _referrers[i] = referrers[_accounts[i]]; + } + return _referrers; + } + + function getReferrer(address _account) external view returns (address) { + return referrers[_account]; + } + + function setStartTimestamp(uint256 _startTimestamp) external onlyOwner { + if (_startTimestamp > endTimestamp) revert InvalidTimestamp(_startTimestamp); + startTimestamp = _startTimestamp; + emit SetStartTimestamp(_startTimestamp); + } + + function setEndTimestamp(uint256 _endTimestamp) external onlyOwner { + if (_endTimestamp < startTimestamp) revert InvalidTimestamp(_endTimestamp); + endTimestamp = _endTimestamp; + emit SetEndTimestamp(_endTimestamp); + } +} diff --git a/src/helpers/SatoshiBORouter.sol b/src/helpers/SatoshiBORouter.sol index 8a20fc8..85bc87d 100644 --- a/src/helpers/SatoshiBORouter.sol +++ b/src/helpers/SatoshiBORouter.sol @@ -7,6 +7,7 @@ import {IBorrowerOperations} from "../interfaces/core/IBorrowerOperations.sol"; import {ITroveManager} from "../interfaces/core/ITroveManager.sol"; import {IDebtToken} from "../interfaces/core/IDebtToken.sol"; import {ISatoshiBORouter} from "./interfaces/ISatoshiBORouter.sol"; +import {IReferralManager} from "./interfaces/IReferralManager.sol"; import {SatoshiMath} from "../dependencies/SatoshiMath.sol"; /** @@ -16,15 +17,23 @@ import {SatoshiMath} from "../dependencies/SatoshiMath.sol"; contract SatoshiBORouter is ISatoshiBORouter { IDebtToken public immutable debtToken; IBorrowerOperations public immutable borrowerOperationsProxy; + IReferralManager public immutable referralManager; IWETH public immutable weth; - constructor(IDebtToken _debtToken, IBorrowerOperations _borrowerOperationsProxy, IWETH _weth) { + constructor( + IDebtToken _debtToken, + IBorrowerOperations _borrowerOperationsProxy, + IReferralManager _referralManager, + IWETH _weth + ) { if (address(_debtToken) == address(0)) revert InvalidZeroAddress(); if (address(_borrowerOperationsProxy) == address(0)) revert InvalidZeroAddress(); + if (address(_referralManager) == address(0)) revert InvalidZeroAddress(); if (address(_weth) == address(0)) revert InvalidZeroAddress(); debtToken = _debtToken; borrowerOperationsProxy = _borrowerOperationsProxy; + referralManager = _referralManager; weth = _weth; } @@ -37,7 +46,8 @@ contract SatoshiBORouter is ISatoshiBORouter { uint256 _collAmount, uint256 _debtAmount, address _upperHint, - address _lowerHint + address _lowerHint, + address _referrer ) external payable { IERC20 collateralToken = troveManager.collateralToken(); @@ -52,7 +62,7 @@ contract SatoshiBORouter is ISatoshiBORouter { uint256 debtTokenBalanceAfter = debtToken.balanceOf(address(this)); uint256 userDebtAmount = debtTokenBalanceAfter - debtTokenBalanceBefore; require(userDebtAmount == _debtAmount, "SatoshiBORouter: Debt amount mismatch"); - _afterWithdrawDebt(userDebtAmount); + _afterWithdrawDebt(account, _referrer, userDebtAmount); } function addColl( @@ -102,7 +112,9 @@ contract SatoshiBORouter is ISatoshiBORouter { uint256 debtTokenBalanceAfter = debtToken.balanceOf(address(this)); uint256 userDebtAmount = debtTokenBalanceAfter - debtTokenBalanceBefore; require(userDebtAmount == _debtAmount, "SatoshiBORouter: Debt amount mismatch"); - _afterWithdrawDebt(userDebtAmount); + + address _referrer = referralManager.getReferrer(account); + _afterWithdrawDebt(account, _referrer, userDebtAmount); } function repayDebt( @@ -162,7 +174,9 @@ contract SatoshiBORouter is ISatoshiBORouter { if (_isDebtIncrease) { uint256 userDebtAmount = debtTokenBalanceAfter - debtTokenBalanceBefore; require(userDebtAmount == _debtChange, "SatoshiBORouter: Debt amount mismatch"); - _afterWithdrawDebt(userDebtAmount); + + address _referrer = referralManager.getReferrer(account); + _afterWithdrawDebt(account, _referrer, userDebtAmount); } } @@ -215,10 +229,13 @@ contract SatoshiBORouter is ISatoshiBORouter { debtToken.transferFrom(msg.sender, address(this), debtAmount); } - function _afterWithdrawDebt(uint256 debtAmount) private { + function _afterWithdrawDebt(address _borrower, address _referrer, uint256 debtAmount) private { if (debtAmount == 0) return; debtToken.transfer(msg.sender, debtAmount); + + // execute referral + referralManager.executeReferral(_borrower, _referrer, debtAmount); } receive() external payable { diff --git a/src/helpers/interfaces/IReferralManager.sol b/src/helpers/interfaces/IReferralManager.sol new file mode 100644 index 0000000..44aa1b9 --- /dev/null +++ b/src/helpers/interfaces/IReferralManager.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import {ISatoshiBORouter} from "./ISatoshiBORouter.sol"; + +interface IReferralManager { + function satoshiBORouter() external view returns (ISatoshiBORouter); + + function startTimestamp() external view returns (uint256); + + function endTimestamp() external view returns (uint256); + + function executeReferral(address _borrower, address _referrer, uint256 _points) external; + + function isReferralActive() external view returns (bool); + + function getTotalPoints() external view returns (uint256); + + function getBatchPoints(address[] calldata _accounts) external view returns (uint256[] memory); + + function getPoints(address _account) external view returns (uint256); + + function getBatchReferrers(address[] calldata _accounts) external view returns (address[] memory); + + function getReferrer(address _account) external view returns (address); + + function setStartTimestamp(uint256 _startTimestamp) external; + + function setEndTimestamp(uint256 _endTimestamp) external; +} diff --git a/src/helpers/interfaces/ISatoshiBORouter.sol b/src/helpers/interfaces/ISatoshiBORouter.sol index 8b17d86..baff6de 100644 --- a/src/helpers/interfaces/ISatoshiBORouter.sol +++ b/src/helpers/interfaces/ISatoshiBORouter.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.13; import {IDebtToken} from "../../interfaces/core/IDebtToken.sol"; import {IBorrowerOperations} from "../../interfaces/core/IBorrowerOperations.sol"; import {ITroveManager} from "../../interfaces/core/ITroveManager.sol"; +import {IReferralManager} from "./IReferralManager.sol"; import {IWETH} from "./IWETH.sol"; interface ISatoshiBORouter { @@ -17,6 +18,8 @@ interface ISatoshiBORouter { function borrowerOperationsProxy() external view returns (IBorrowerOperations); + function referralManager() external view returns (IReferralManager); + function weth() external view returns (IWETH); function openTrove( @@ -26,7 +29,8 @@ interface ISatoshiBORouter { uint256 _collAmount, uint256 _debtAmount, address _upperHint, - address _lowerHint + address _lowerHint, + address _referrer ) external payable; function addColl( diff --git a/test/ReferralManager.t.sol b/test/ReferralManager.t.sol new file mode 100644 index 0000000..6efaf92 --- /dev/null +++ b/test/ReferralManager.t.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {ISortedTroves} from "../src/interfaces/core/ISortedTroves.sol"; +import {ITroveManager, TroveManagerOperation} from "../src/interfaces/core/ITroveManager.sol"; +import {ISatoshiBORouter} from "../src/helpers/interfaces/ISatoshiBORouter.sol"; +import {IMultiCollateralHintHelpers} from "../src/helpers/interfaces/IMultiCollateralHintHelpers.sol"; +import {IWETH} from "../src/helpers/interfaces/IWETH.sol"; +import {IReferralManager} from "../src/helpers/interfaces/IReferralManager.sol"; +import {SatoshiMath} from "../src/dependencies/SatoshiMath.sol"; +import {DeployBase, LocalVars} from "./utils/DeployBase.t.sol"; +import {HintLib} from "./utils/HintLib.sol"; +import {DEPLOYER, OWNER, GAS_COMPENSATION, TestConfig} from "./TestConfig.sol"; +import {TroveBase} from "./utils/TroveBase.t.sol"; +import {Events} from "./utils/Events.sol"; + +contract ReferralManagerTest is Test, DeployBase, TroveBase, TestConfig, Events { + using Math for uint256; + + ISortedTroves sortedTrovesBeaconProxy; + ITroveManager troveManagerBeaconProxy; + IWETH weth; + IMultiCollateralHintHelpers hintHelpers; + ISatoshiBORouter satoshiBORouter; + IReferralManager referralManager; + address user; + address referrer; + + function setUp() public override { + super.setUp(); + + // testing user + user = vm.addr(1); + referrer = vm.addr(2); + + // use WETH as collateral + weth = IWETH(_deployWETH(DEPLOYER)); + deal(address(weth), 10000e18); + + // setup contracts and deploy one instance + (sortedTrovesBeaconProxy, troveManagerBeaconProxy) = _deploySetupAndInstance( + DEPLOYER, OWNER, ORACLE_MOCK_DECIMALS, ORACLE_MOCK_VERSION, initRoundData, weth, deploymentParams + ); + + // deploy helper contracts + hintHelpers = IMultiCollateralHintHelpers(_deployHintHelpers(DEPLOYER)); + + uint64 nonce = vm.getNonce(DEPLOYER); + address cpSatoshiBORouterAddr = vm.computeCreateAddress(DEPLOYER, nonce); + address cpReferralManagerAddr = vm.computeCreateAddress(DEPLOYER, ++nonce); + satoshiBORouter = + ISatoshiBORouter(_deploySatoshiBORouter(DEPLOYER, IReferralManager(cpReferralManagerAddr), weth)); + referralManager = IReferralManager(_deployReferralManager(DEPLOYER, ISatoshiBORouter(cpSatoshiBORouterAddr))); + + // user set delegate approval for satoshiBORouter + vm.startPrank(user); + borrowerOperationsProxy.setDelegateApproval(address(satoshiBORouter), true); + vm.stopPrank(); + } + + function testOpenTroveReferral() public { + LocalVars memory vars; + // open trove params + vars.collAmt = 1e18; // price defined in `TestConfig.roundData` + vars.debtAmt = 10000e18; // 10000 USD + + vm.startPrank(user); + deal(user, 1e18); + + // state before + uint256 totalPointsBefore = referralManager.getTotalPoints(); + uint256 referrerPointsBefore = referralManager.getPoints(referrer); + + /* check events emitted correctly in tx */ + // check ExecuteReferral event + vm.expectEmit(true, true, true, true, address(referralManager)); + emit ExecuteReferral(user, referrer, vars.debtAmt); + + // calc hint + (vars.upperHint, vars.lowerHint) = HintLib.getHint( + hintHelpers, sortedTrovesBeaconProxy, troveManagerBeaconProxy, vars.collAmt, vars.debtAmt, GAS_COMPENSATION + ); + // tx execution + satoshiBORouter.openTrove{value: vars.collAmt}( + troveManagerBeaconProxy, + user, + 0.05e18, /* vars.maxFeePercentage 5% */ + vars.collAmt, + vars.debtAmt, + vars.upperHint, + vars.lowerHint, + referrer + ); + + // state after + uint256 totalPointsAfter = referralManager.getTotalPoints(); + uint256 referrerPointsAfter = referralManager.getPoints(referrer); + + // check state + assertEq(totalPointsAfter, totalPointsBefore + vars.debtAmt); + assertEq(referrerPointsAfter, referrerPointsBefore + vars.debtAmt); + + vm.stopPrank(); + } + + function testWithdrawDebtReferral() public { + LocalVars memory vars; + // pre open trove + vars.collAmt = 1e18; // price defined in `TestConfig.roundData` + vars.debtAmt = 10000e18; // 10000 USD + vars.maxFeePercentage = 0.05e18; // 5% + satoshiBORouter.openTrove{value: vars.collAmt}( + troveManagerBeaconProxy, + user, + 0.05e18, /* vars.maxFeePercentage 5% */ + vars.collAmt, + vars.debtAmt, + vars.upperHint, + vars.lowerHint, + referrer + ); + + vars.withdrawDebtAmt = 10000e18; + vars.totalNetDebtAmt = vars.debtAmt + vars.withdrawDebtAmt; + + vm.startPrank(user); + + // state before + uint256 totalPointsBefore = referralManager.getTotalPoints(); + uint256 referrerPointsBefore = referralManager.getPoints(referrer); + + /* check events emitted correctly in tx */ + // check ExecuteReferral event + vm.expectEmit(true, true, true, true, address(referralManager)); + emit ExecuteReferral(user, referrer, vars.withdrawDebtAmt); + + // calc hint + (vars.upperHint, vars.lowerHint) = HintLib.getHint( + hintHelpers, + sortedTrovesBeaconProxy, + troveManagerBeaconProxy, + vars.collAmt, + vars.totalNetDebtAmt, + GAS_COMPENSATION + ); + // tx execution + satoshiBORouter.withdrawDebt( + troveManagerBeaconProxy, user, vars.maxFeePercentage, vars.withdrawDebtAmt, vars.upperHint, vars.lowerHint + ); + + // state after + uint256 totalPointsAfter = referralManager.getTotalPoints(); + uint256 referrerPointsAfter = referralManager.getPoints(referrer); + + // check state + assertEq(totalPointsAfter, totalPointsBefore + vars.withdrawDebtAmt); + assertEq(referrerPointsAfter, referrerPointsBefore + vars.withdrawDebtAmt); + + vm.stopPrank(); + } + + function testAdjustTroveReferral_AddCollAndWithdrawDebt() public { + LocalVars memory vars; + // pre open trove + vars.collAmt = 1e18; // price defined in `TestConfig.roundData` + vars.debtAmt = 10000e18; // 10000 USD + vars.maxFeePercentage = 0.05e18; // 5% + satoshiBORouter.openTrove{value: vars.collAmt}( + troveManagerBeaconProxy, + user, + 0.05e18, /* vars.maxFeePercentage 5% */ + vars.collAmt, + vars.debtAmt, + vars.upperHint, + vars.lowerHint, + referrer + ); + + vars.addCollAmt = 0.5e18; + vars.withdrawDebtAmt = 5000e18; + vars.totalCollAmt = vars.collAmt + vars.addCollAmt; + vars.totalNetDebtAmt = vars.debtAmt + vars.withdrawDebtAmt; + + vm.startPrank(user); + deal(user, vars.addCollAmt); + + // state before + uint256 totalPointsBefore = referralManager.getTotalPoints(); + uint256 referrerPointsBefore = referralManager.getPoints(referrer); + + /* check events emitted correctly in tx */ + // check ExecuteReferral event + vm.expectEmit(true, true, true, true, address(referralManager)); + emit ExecuteReferral(user, referrer, vars.withdrawDebtAmt); + + // calc hint + (vars.upperHint, vars.lowerHint) = HintLib.getHint( + hintHelpers, + sortedTrovesBeaconProxy, + troveManagerBeaconProxy, + vars.totalCollAmt, + vars.totalNetDebtAmt, + GAS_COMPENSATION + ); + + // tx execution + satoshiBORouter.adjustTrove{value: vars.addCollAmt}( + troveManagerBeaconProxy, + user, + vars.maxFeePercentage, + vars.addCollAmt, + 0, /* collWithdrawalAmt */ + vars.withdrawDebtAmt, + true, /* debtIncrease */ + vars.upperHint, + vars.lowerHint + ); + + // state after + uint256 totalPointsAfter = referralManager.getTotalPoints(); + uint256 referrerPointsAfter = referralManager.getPoints(referrer); + + // check state + assertEq(totalPointsAfter, totalPointsBefore + vars.withdrawDebtAmt); + assertEq(referrerPointsAfter, referrerPointsBefore + vars.withdrawDebtAmt); + + vm.stopPrank(); + } + + function testAdjustTroveReferral_WithdrawCollAndWithdrawDebt() public { + LocalVars memory vars; + // pre open trove + vars.collAmt = 1e18; // price defined in `TestConfig.roundData` + vars.debtAmt = 10000e18; // 10000 USD + vars.maxFeePercentage = 0.05e18; // 5% + satoshiBORouter.openTrove{value: vars.collAmt}( + troveManagerBeaconProxy, + user, + 0.05e18, /* vars.maxFeePercentage 5% */ + vars.collAmt, + vars.debtAmt, + vars.upperHint, + vars.lowerHint, + referrer + ); + + vars.withdrawCollAmt = 0.5e18; + vars.withdrawDebtAmt = 2000e18; + vars.totalCollAmt = vars.collAmt - vars.withdrawCollAmt; + vars.totalNetDebtAmt = vars.debtAmt + vars.withdrawDebtAmt; + + vm.startPrank(user); + + // state before + uint256 totalPointsBefore = referralManager.getTotalPoints(); + uint256 referrerPointsBefore = referralManager.getPoints(referrer); + + /* check events emitted correctly in tx */ + // check ExecuteReferral event + vm.expectEmit(true, true, true, true, address(referralManager)); + emit ExecuteReferral(user, referrer, vars.withdrawDebtAmt); + + // calc hint + (vars.upperHint, vars.lowerHint) = HintLib.getHint( + hintHelpers, + sortedTrovesBeaconProxy, + troveManagerBeaconProxy, + vars.totalCollAmt, + vars.totalNetDebtAmt, + GAS_COMPENSATION + ); + + // tx execution + satoshiBORouter.adjustTrove( + troveManagerBeaconProxy, + user, + vars.maxFeePercentage, + 0, /* collAdditionAmt */ + vars.withdrawCollAmt, + vars.withdrawDebtAmt, + true, /* debtIncrease */ + vars.upperHint, + vars.lowerHint + ); + + // state after + uint256 totalPointsAfter = referralManager.getTotalPoints(); + uint256 referrerPointsAfter = referralManager.getPoints(referrer); + + // check state + assertEq(totalPointsAfter, totalPointsBefore + vars.withdrawDebtAmt); + assertEq(referrerPointsAfter, referrerPointsBefore + vars.withdrawDebtAmt); + + vm.stopPrank(); + } + + function testFailExecuteReferral() public { + vm.startPrank(user); + + referralManager.executeReferral(user, user, 10000); + + vm.stopPrank(); + } + + function testOpenTroveNotInReferralTime() public { + // reset referral start and end time + vm.startPrank(DEPLOYER); + referralManager.setStartTimestamp(0); + referralManager.setEndTimestamp(0); + vm.stopPrank(); + + LocalVars memory vars; + // open trove params + vars.collAmt = 1e18; // price defined in `TestConfig.roundData` + vars.debtAmt = 10000e18; // 10000 USD + + vm.startPrank(user); + deal(user, 1e18); + + // state before + uint256 totalPointsBefore = referralManager.getTotalPoints(); + uint256 referrerPointsBefore = referralManager.getPoints(referrer); + + // calc hint + (vars.upperHint, vars.lowerHint) = HintLib.getHint( + hintHelpers, sortedTrovesBeaconProxy, troveManagerBeaconProxy, vars.collAmt, vars.debtAmt, GAS_COMPENSATION + ); + // tx execution + satoshiBORouter.openTrove{value: vars.collAmt}( + troveManagerBeaconProxy, + user, + 0.05e18, /* vars.maxFeePercentage 5% */ + vars.collAmt, + vars.debtAmt, + vars.upperHint, + vars.lowerHint, + referrer + ); + + // state after + uint256 totalPointsAfter = referralManager.getTotalPoints(); + uint256 referrerPointsAfter = referralManager.getPoints(referrer); + + // check state + assertEq(totalPointsAfter, totalPointsBefore); + assertEq(referrerPointsAfter, referrerPointsBefore); + + vm.stopPrank(); + } +} diff --git a/test/SatoshiBORouter.t.sol b/test/SatoshiBORouter.t.sol index d25e267..4f03f11 100644 --- a/test/SatoshiBORouter.t.sol +++ b/test/SatoshiBORouter.t.sol @@ -9,6 +9,7 @@ import {ITroveManager, TroveManagerOperation} from "../src/interfaces/core/ITrov import {ISatoshiBORouter} from "../src/helpers/interfaces/ISatoshiBORouter.sol"; import {IMultiCollateralHintHelpers} from "../src/helpers/interfaces/IMultiCollateralHintHelpers.sol"; import {IWETH} from "../src/helpers/interfaces/IWETH.sol"; +import {IReferralManager} from "../src/helpers/interfaces/IReferralManager.sol"; import {SatoshiMath} from "../src/dependencies/SatoshiMath.sol"; import {DeployBase, LocalVars} from "./utils/DeployBase.t.sol"; import {HintLib} from "./utils/HintLib.sol"; @@ -24,13 +25,16 @@ contract SatoshiBORouterTest is Test, DeployBase, TroveBase, TestConfig, Events IWETH weth; IMultiCollateralHintHelpers hintHelpers; ISatoshiBORouter satoshiBORouter; + IReferralManager referralManager; address user; + address referrer; function setUp() public override { super.setUp(); // testing user user = vm.addr(1); + referrer = vm.addr(2); // use WETH as collateral weth = IWETH(_deployWETH(DEPLOYER)); @@ -43,7 +47,13 @@ contract SatoshiBORouterTest is Test, DeployBase, TroveBase, TestConfig, Events // deploy helper contracts hintHelpers = IMultiCollateralHintHelpers(_deployHintHelpers(DEPLOYER)); - satoshiBORouter = ISatoshiBORouter(_deploySatoshiBORouter(DEPLOYER, weth)); + + uint64 nonce = vm.getNonce(DEPLOYER); + address cpSatoshiBORouterAddr = vm.computeCreateAddress(DEPLOYER, nonce); + address cpReferralManagerAddr = vm.computeCreateAddress(DEPLOYER, ++nonce); + satoshiBORouter = + ISatoshiBORouter(_deploySatoshiBORouter(DEPLOYER, IReferralManager(cpReferralManagerAddr), weth)); + referralManager = IReferralManager(_deployReferralManager(DEPLOYER, ISatoshiBORouter(cpSatoshiBORouterAddr))); // user set delegate approval for satoshiBORouter vm.startPrank(user); @@ -102,7 +112,8 @@ contract SatoshiBORouterTest is Test, DeployBase, TroveBase, TestConfig, Events vars.collAmt, vars.debtAmt, vars.upperHint, - vars.lowerHint + vars.lowerHint, + referrer ); // state after diff --git a/test/UpgradeContract.t.sol b/test/UpgradeContract.t.sol index 35c2725..7a00a97 100644 --- a/test/UpgradeContract.t.sol +++ b/test/UpgradeContract.t.sol @@ -11,11 +11,10 @@ import {IFactory} from "../src/interfaces/core/IFactory.sol"; import {IBorrowerOperations} from "../src/interfaces/core/IBorrowerOperations.sol"; import {BorrowerOperations} from "../src/core/BorrowerOperations.sol"; import {DeployBase} from "./utils/DeployBase.t.sol"; -import {DEPLOYER, OWNER, GAS_COMPENSATION, BO_MIN_NET_DEBT,TestConfig} from "./TestConfig.sol"; +import {DEPLOYER, OWNER, GAS_COMPENSATION, BO_MIN_NET_DEBT, TestConfig} from "./TestConfig.sol"; import {Events} from "./utils/Events.sol"; contract UpgradteContractTest is Test, DeployBase, TestConfig, Events { - ISortedTroves sortedTrovesBeaconProxy; ITroveManager troveManagerBeaconProxy; address user1; @@ -41,11 +40,13 @@ contract UpgradteContractTest is Test, DeployBase, TestConfig, Events { // upgrade to new borrower operations implementation BorrowerOperations borrowerOperationsProxy = BorrowerOperations(address(borrowerOperationsProxy)); borrowerOperationsProxy.upgradeTo(address(newBorrowerOperationsImpl)); - bytes32 s = vm.load(address(borrowerOperationsProxy), BorrowerOperations(address(newBorrowerOperationsImpl)).proxiableUUID()); + bytes32 s = vm.load( + address(borrowerOperationsProxy), BorrowerOperations(address(newBorrowerOperationsImpl)).proxiableUUID() + ); // `0x000...address` << 96 -> `0xaddress000...000` s <<= 96; assertEq(s, bytes32(bytes20(address(newBorrowerOperationsImpl)))); - + vm.stopPrank(); } } diff --git a/test/utils/DeployBase.t.sol b/test/utils/DeployBase.t.sol index 5da0e21..4f849e9 100644 --- a/test/utils/DeployBase.t.sol +++ b/test/utils/DeployBase.t.sol @@ -24,6 +24,7 @@ import {Factory, DeploymentParams} from "../../src/core/Factory.sol"; import {RoundData, OracleMock} from "../../src/mocks/OracleMock.sol"; import {PriceFeedChainlink} from "../../src/dependencies/priceFeed/PriceFeedChainlink.sol"; import {AggregatorV3Interface} from "../../src/interfaces/dependencies/priceFeed/AggregatorV3Interface.sol"; +import {ReferralManager} from "../../src/helpers/ReferralManager.sol"; import {IWETH} from "../../src/helpers/interfaces/IWETH.sol"; import {ISortedTroves} from "../../src/interfaces/core/ISortedTroves.sol"; import {IPriceFeedAggregator} from "../../src/interfaces/core/IPriceFeedAggregator.sol"; @@ -36,6 +37,8 @@ import {ISatoshiCore} from "../../src/interfaces/core/ISatoshiCore.sol"; import {IDebtToken} from "../../src/interfaces/core/IDebtToken.sol"; import {IFactory} from "../../src/interfaces/core/IFactory.sol"; import {IPriceFeed} from "../../src/interfaces/dependencies/IPriceFeed.sol"; +import {ISatoshiBORouter} from "../../src/helpers/interfaces/ISatoshiBORouter.sol"; +import {IReferralManager} from "../../src/helpers/interfaces/IReferralManager.sol"; import { DEPLOYER, OWNER, @@ -473,17 +476,37 @@ abstract contract DeployBase is Test { return wethAddr; } - function _deploySatoshiBORouter(address deployer, IWETH weth) internal returns (address) { + function _deploySatoshiBORouter(address deployer, IReferralManager referralManager, IWETH weth) + internal + returns (address) + { vm.startPrank(deployer); assert(debtToken != IDebtToken(address(0))); // check if debt token contract is deployed assert(borrowerOperationsProxy != IBorrowerOperations(address(0))); // check if borrower operations proxy contract is deployed + assert(referralManager != IReferralManager(address(0))); // check if referral manager contract is not zero address assert(weth != IWETH(address(0))); // check if WETH contract is deployed - address satoshiBORouterAddr = address(new SatoshiBORouter(debtToken, borrowerOperationsProxy, weth)); + address satoshiBORouterAddr = + address(new SatoshiBORouter(debtToken, borrowerOperationsProxy, referralManager, weth)); vm.stopPrank(); return satoshiBORouterAddr; } + function _deployReferralManager(address deployer, ISatoshiBORouter satoshiBORouter) internal returns (address) { + vm.startPrank(deployer); + assert(satoshiBORouter != ISatoshiBORouter(address(0))); // check if satoshiBORouter contract is not zero address + uint256 startTimestamp = block.timestamp; + uint256 endTimestamp = startTimestamp + 30 days; + address referralManagerAddr = address(new ReferralManager(satoshiBORouter, startTimestamp, endTimestamp)); + assert(IReferralManager(referralManagerAddr).satoshiBORouter() == satoshiBORouter); + assert(IReferralManager(referralManagerAddr).startTimestamp() == startTimestamp); + assert(IReferralManager(referralManagerAddr).endTimestamp() == endTimestamp); + assert(IReferralManager(referralManagerAddr).getTotalPoints() == 0); + vm.stopPrank(); + + return referralManagerAddr; + } + /* ============ Deploy DebtTokenTester Contracts ============ */ function _deployDebtTokenTester() internal { vm.startPrank(DEPLOYER); diff --git a/test/utils/Events.sol b/test/utils/Events.sol index ae17200..3129089 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -18,4 +18,7 @@ abstract contract Events { address indexed _borrower, uint256 _debt, uint256 _coll, uint256 _stake, TroveManagerOperation _operation ); event TotalStakesUpdated(uint256 _newTotalStakes); + + // ReferralManager + event ExecuteReferral(address indexed borrower, address indexed referrer, uint256 points); }