diff --git a/contracts/AggregatorDataProvider.sol b/contracts/AggregatorDataProvider.sol index 3c45ece..1aeb1ec 100644 --- a/contracts/AggregatorDataProvider.sol +++ b/contracts/AggregatorDataProvider.sol @@ -25,6 +25,7 @@ contract AggregatorDataProvider is uint256 public constant GANACHE = 1337; uint256 public constant GANACHE2 = 1234; uint256 public constant MUMBAI = 80001; + uint256 public constant AMOY = 80002; AggregatorV2V3Interface private _aggregator; @@ -297,6 +298,7 @@ contract AggregatorDataProvider is { return (block.chainid == GANACHE) || (block.chainid == GANACHE2) - || (block.chainid == MUMBAI); + || (block.chainid == MUMBAI) + || (block.chainid == AMOY); } } diff --git a/contracts/DepegDistribution.sol b/contracts/DepegDistribution.sol new file mode 100644 index 0000000..2a4458b --- /dev/null +++ b/contracts/DepegDistribution.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.2; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@etherisc/gif-interface/contracts/modules/IRegistry.sol"; +import "@etherisc/gif-interface/contracts/services/IInstanceService.sol"; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {DepegProduct} from "./DepegProduct.sol"; +import {DepegRiskpool} from "./DepegRiskpool.sol"; + +contract DepegDistribution is + Ownable +{ + struct DistributorInfo { + uint256 commissionRate; + uint256 commissionBalance; + uint256 policiesSold; + uint256 createdAt; + uint256 updatedAt; + } + + event LogDepegPolicySold(address distributor, bytes32 processId, uint256 premiumTotalAmount, address protectedWallet, uint256 protectedBalance); + event LogDistributionInfoUpdated(address distributor, uint256 commissionAmount, uint256 commissionBalance, uint256 totalPoliciesSold); + + uint8 public constant DECIMALS = 18; + uint256 public constant COMMISSION_RATE_DEFAULT = 5 * 10 ** (DECIMALS - 2); + uint256 public constant COMMISSION_RATE_MAX = 33 * 10 ** (DECIMALS - 2); + + DepegProduct private _depegProduct; + DepegRiskpool private _depegRiskpool; + IERC20Metadata private _token; + address private _treasury; + + mapping(address => DistributorInfo) private _distributor; + address [] private _distributors; + + + modifier onlyDistributor() { + require( + isDistributor(msg.sender), + "ERROR:DST-001:NOT_DISTRIBUTOR" + ); + _; + } + + constructor( + address depegProduct, + uint256 productId + ) + Ownable() + { + _depegProduct = DepegProduct(depegProduct); + require(_depegProduct.getId() == productId, "ERROR:DST-010:PRODUCT_ID_MISMATCH"); + + IRegistry registry = IRegistry(_depegProduct.getRegistry()); + IInstanceService instanceService = IInstanceService(registry.getContract("InstanceService")); + + _depegRiskpool = DepegRiskpool( + address(instanceService.getComponent( + _depegProduct.getRiskpoolId()))); + + _token = IERC20Metadata(_depegProduct.getToken()); + _treasury = instanceService.getTreasuryAddress(); + } + + function createDistributor(address distributor) + external + onlyOwner() + returns (DistributorInfo memory) + { + require(!isDistributor(distributor), "ERROR:DST-020:DISTRIBUTOR_ALREADY_EXISTS"); + + _distributor[distributor] = DistributorInfo( + COMMISSION_RATE_DEFAULT, + 0, // commissionAmount, + 0, // policiesSold + block.timestamp, // createdAt + block.timestamp // updatedAt + ); + + _distributors.push(distributor); + + return _distributor[distributor]; + } + + function setCommissionRate( + address distributor, + uint256 commissionRate + ) + external + onlyOwner() + { + require(isDistributor(distributor), "ERROR:DST-030:NOT_DISTRIBUTOR"); + require(commissionRate <= COMMISSION_RATE_MAX, "ERROR:DST-031:COMMISSION_RATE_TOO_HIGH"); + + DistributorInfo storage info = _distributor[distributor]; + info.commissionRate = commissionRate; + info.updatedAt = block.timestamp; + } + + + /// @dev lets a distributor create a policy for the specified wallet address + // the policy holder is this contract, the beneficiary is the specified wallet address + function createPolicy( + address buyer, + address protectedWallet, + uint256 protectedBalance, + uint256 duration, + uint256 bundleId + ) + external + onlyDistributor() + returns(bytes32 processId) + { + // collect premium and commission from buyer to this contract + ( + uint256 premiumTotalAmount, + uint256 premiumNetAmount, + ) = _collectTokenAndUpdateCommission( + buyer, + protectedBalance, + duration, + bundleId); + + // create allowance for net premium + SafeERC20.safeIncreaseAllowance(_token, _treasury, premiumNetAmount); + + // create policy + // this will transfer premium amount from this contract to depeg (and keep the commission in this contract) + processId = _depegProduct.applyForPolicyWithBundle( + protectedWallet, + protectedBalance, + duration, + bundleId); + + emit LogDepegPolicySold(msg.sender, processId, premiumTotalAmount, protectedWallet, protectedBalance); + } + + function _collectTokenAndUpdateCommission( + address buyer, + uint256 protectedBalance, + uint256 duration, + uint256 bundleId + ) + internal + returns ( + uint256 premiumTotalAmount, + uint256 premiumNetAmount, + uint256 commissionAmount + ) + { + address distributor = msg.sender; + + // calculate amounts + ( + premiumTotalAmount, + commissionAmount + ) = calculatePrice(distributor, protectedBalance, duration, bundleId); + + premiumNetAmount = premiumTotalAmount - commissionAmount; + + // update distributor book keeping record + DistributorInfo storage info = _distributor[distributor]; + info.commissionBalance += commissionAmount; + info.policiesSold += 1; + info.updatedAt = block.timestamp; + + // collect total premium amount + SafeERC20.safeTransferFrom(_token, buyer, address (this), premiumTotalAmount); + + emit LogDistributionInfoUpdated(distributor, commissionAmount, info.commissionBalance, info.policiesSold); + } + + + function calculatePrice( + address distributor, + uint256 protectedBalance, + uint256 duration, + uint256 bundleId + ) + public + view + returns ( + uint256 premiumTotalAmount, + uint256 commissionAmount + ) + { + // fetch policy price + uint256 sumInsured = _depegRiskpool.calculateSumInsured(protectedBalance); + uint256 netPremium = _depegProduct.calculateNetPremium( + sumInsured, + duration, + bundleId); + + uint256 depegPremium = _depegProduct.calculatePremium(netPremium); + + // calculate commission and total premium + commissionAmount = calculateCommission(distributor, depegPremium); + premiumTotalAmount = depegPremium + commissionAmount; + } + + function calculateCommission(address distributor, uint256 netPremiumAmount) + public + view + returns(uint256 commissionAmount) + { + uint256 rate = _distributor[distributor].commissionRate; + if(rate == 0) { + return 0; + } + + return (netPremiumAmount * rate) / (10**DECIMALS - rate); + } + + /// @dev distribution owner "override" to potentially collect commissions that + /// that is not collected by + function withdraw(uint256 amount) + external + onlyOwner() + { + require(_token.balanceOf(address(this)) >= amount, "ERROR:DST-040:BALANCE_INSUFFICIENT"); + SafeERC20.safeTransfer(_token, owner(), amount); + } + + function withdrawCommission(uint256 amount) + external + onlyDistributor() + { + address distributor = msg.sender; + require(getCommissionBalance(distributor) >= amount, "ERROR:DST-050:AMOUNT_TOO_LARGE"); + require(_token.balanceOf(address(this)) >= amount, "ERROR:DST-051:BALANCE_INSUFFICIENT"); + + // update distributor book keeping record + DistributorInfo storage info = _distributor[distributor]; + info.commissionBalance -= amount; + info.updatedAt = block.timestamp; + + SafeERC20.safeTransfer(_token, distributor, amount); + } + + function getToken() external view returns (address token) { + return address(_token); + } + + function distributors() external view returns(uint256) { + return _distributors.length; + } + + function getDistributor(uint256 idx) external view returns(address) { + return _distributors[idx]; + } + + function isDistributor(address distributor) public view returns (bool) { + return _distributor[distributor].createdAt > 0; + } + + function getPoliciesSold(address distributor) external view returns (uint256 policies) { + return _distributor[distributor].policiesSold; + } + + function getCommissionBalance(address distributor) public view returns (uint256 commissionAmount) { + return _distributor[distributor].commissionBalance; + } + + function getCommissionRate(address distributor) external view returns (uint256 commissionRate) { + return _distributor[distributor].commissionRate; + } + + function getDistributorInfo(address distributor) external view returns (DistributorInfo memory) { + return _distributor[distributor]; + } +} \ No newline at end of file diff --git a/contracts/DepegRiskpool.sol b/contracts/DepegRiskpool.sol index c9197af..22892b4 100644 --- a/contracts/DepegRiskpool.sol +++ b/contracts/DepegRiskpool.sol @@ -363,6 +363,36 @@ contract DepegRiskpool is } + function _afterCloseBundle(uint256 bundleId) + internal + override + { + super._afterCloseBundle(bundleId); + _syncBundleExpiryWithCurrentTime(bundleId); + } + + function _afterBurnBundle(uint256 bundleId) + internal + override + { + _syncBundleExpiryWithCurrentTime(bundleId); + } + + function _syncBundleExpiryWithCurrentTime(uint256 bundleId) + internal + { + if (address(_chainRegistry) == address(0) || _bundleNftId[bundleId] == 0) { + return; + } + + uint96 nftId = getNftId(bundleId); + (,,,,, uint256 expiryAt) = _chainRegistry.decodeBundleData(nftId); + + if (expiryAt > block.timestamp) { + _chainRegistry.setBundleExpiryAt(nftId, block.timestamp); + } + } + function getSumInsuredPercentage() external view diff --git a/contracts/experiment/IUniswapV2Router.sol b/contracts/experiment/IUniswapV2Router.sol new file mode 100644 index 0000000..339d1aa --- /dev/null +++ b/contracts/experiment/IUniswapV2Router.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +// Interface for Uniswap V2 Router +interface IUniswapV2Router { + function swapExactETHForTokens( + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function WETH() external pure returns (address); +} \ No newline at end of file diff --git a/contracts/experiment/Usdt.sol b/contracts/experiment/Usdt.sol new file mode 100644 index 0000000..1b73cb7 --- /dev/null +++ b/contracts/experiment/Usdt.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.2; + + +/** + * @title SafeMath + * @dev Math operations with safety checks that throw on error + */ +library SafeMath { + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + uint256 c = a * b; + assert(c / a == b); + return c; + } + + function div(uint256 a, uint256 b) internal pure returns (uint256) { + // assert(b > 0); // Solidity automatically throws when dividing by 0 + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + return c; + } + + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + assert(b <= a); + return a - b; + } + + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + assert(c >= a); + return c; + } +} + +/** + * @title Ownable + * @dev The Ownable contract has an owner address, and provides basic authorization control + * functions, this simplifies the implementation of "user permissions". + */ +contract Ownable { + address public owner; + + /** + * @dev The Ownable constructor sets the original `owner` of the contract to the sender + * account. + */ + constructor() { + owner = msg.sender; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + /** + * @dev Allows the current owner to transfer control of the contract to a newOwner. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + if (newOwner != address(0)) { + owner = newOwner; + } + } + +} + +/** + * @title ERC20Basic + * @dev Simpler version of ERC20 interface + * @dev see https://github.com/ethereum/EIPs/issues/20 + */ +abstract contract ERC20Basic { + uint public _totalSupply; + function totalSupply() public virtual view returns (uint); + function balanceOf(address who) public virtual view returns (uint); + function transfer(address to, uint value) public virtual; + event Transfer(address indexed from, address indexed to, uint value); +} + +/** + * @title ERC20 interface + * @dev see https://github.com/ethereum/EIPs/issues/20 + */ +abstract contract ERC20 is ERC20Basic { + function allowance(address owner, address spender) public virtual view returns (uint); + function transferFrom(address from, address to, uint value) public virtual; + function approve(address spender, uint value) public virtual; + event Approval(address indexed owner, address indexed spender, uint value); +} + +/** + * @title Basic token + * @dev Basic version of StandardToken, with no allowances. + */ +abstract contract BasicToken is Ownable, ERC20Basic { + using SafeMath for uint; + + mapping(address => uint) public balances; + + // additional variables for use if transaction fees ever became necessary + uint public basisPointsRate = 0; + uint public maximumFee = 0; + + /** + * @dev Fix for the ERC20 short address attack. + */ + modifier onlyPayloadSize(uint size) { + require(!(msg.data.length < size + 4)); + _; + } + + /** + * @dev transfer token for a specified address + * @param _to The address to transfer to. + * @param _value The amount to be transferred. + */ + function transfer(address _to, uint _value) public virtual override onlyPayloadSize(2 * 32) { + uint fee = (_value.mul(basisPointsRate)).div(10000); + if (fee > maximumFee) { + fee = maximumFee; + } + uint sendAmount = _value.sub(fee); + balances[msg.sender] = balances[msg.sender].sub(_value); + balances[_to] = balances[_to].add(sendAmount); + if (fee > 0) { + balances[owner] = balances[owner].add(fee); + emit Transfer(msg.sender, owner, fee); + } + emit Transfer(msg.sender, _to, sendAmount); + } + + /*** + * @dev Gets the balance of the specified address. + * @param _owner The address to query the the balance of. + * @return An uint representing the amount owned by the passed address. + */ + function balanceOf(address _owner) public virtual override view returns (uint balance) { + return balances[_owner]; + } + +} + +/** + * @title Standard ERC20 token + * + * @dev Implementation of the basic standard token. + * @dev https://github.com/ethereum/EIPs/issues/20 + * @dev Based oncode by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol + */ +abstract contract StandardToken is BasicToken, ERC20 { + + using SafeMath for uint256; + + mapping (address => mapping (address => uint)) public allowed; + + uint public constant MAX_UINT = 2**256 - 1; + + /** + * @dev Transfer tokens from one address to another + * @param _from address The address which you want to send tokens from + * @param _to address The address which you want to transfer to + * @param _value uint the amount of tokens to be transferred + */ + function transferFrom(address _from, address _to, uint _value) public virtual override onlyPayloadSize(3 * 32) { + uint _allowance = allowed[_from][msg.sender]; + + // Check is not needed because sub(_allowance, _value) will already throw if this condition is not met + // if (_value > _allowance) throw; + + uint fee = (_value.mul(basisPointsRate)).div(10000); + if (fee > maximumFee) { + fee = maximumFee; + } + if (_allowance < MAX_UINT) { + allowed[_from][msg.sender] = _allowance.sub(_value); + } + uint sendAmount = _value.sub(fee); + balances[_from] = balances[_from].sub(_value); + balances[_to] = balances[_to].add(sendAmount); + if (fee > 0) { + balances[owner] = balances[owner].add(fee); + emit Transfer(_from, owner, fee); + } + emit Transfer(_from, _to, sendAmount); + } + + /** + * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + * @param _spender The address which will spend the funds. + * @param _value The amount of tokens to be spent. + */ + function approve(address _spender, uint _value) public virtual override onlyPayloadSize(2 * 32) { + + // To change the approve amount you first have to reduce the addresses` + // allowance to zero by calling `approve(_spender, 0)` if it is not + // already 0 to mitigate the race condition described here: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + require(!((_value != 0) && (allowed[msg.sender][_spender] != 0))); + + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + } + + /*** + * @dev Function to check the amount of tokens than an owner allowed to a spender. + * @param _owner address The address which owns the funds. + * @param _spender address The address which will spend the funds. + * @return A uint specifying the amount of tokens still available for the spender. + */ + function allowance(address _owner, address _spender) public virtual override view returns (uint remaining) { + return allowed[_owner][_spender]; + } + +} + + +/** + * @title Pausable + * @dev Base contract which allows children to implement an emergency stop mechanism. + */ +contract Pausable is Ownable { + event Pause(); + event Unpause(); + + bool public paused = false; + + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + */ + modifier whenNotPaused() { + require(!paused); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + */ + modifier whenPaused() { + require(paused); + _; + } + + /** + * @dev called by the owner to pause, triggers stopped state + */ + function pause() onlyOwner whenNotPaused public { + paused = true; + Pause(); + } + + /** + * @dev called by the owner to unpause, returns to normal state + */ + function unpause() onlyOwner whenPaused public { + paused = false; + Unpause(); + } +} + +abstract contract BlackList is Ownable, BasicToken { + + /////// Getters to allow the same blacklist to be used also by other contracts (including upgraded Tether) /////// + function getBlackListStatus(address _maker) external view returns (bool) { + return isBlackListed[_maker]; + } + + function getOwner() external view returns (address) { + return owner; + } + + mapping (address => bool) public isBlackListed; + + function addBlackList (address _evilUser) public onlyOwner { + isBlackListed[_evilUser] = true; + AddedBlackList(_evilUser); + } + + function removeBlackList (address _clearedUser) public onlyOwner { + isBlackListed[_clearedUser] = false; + RemovedBlackList(_clearedUser); + } + + function destroyBlackFunds (address _blackListedUser) public onlyOwner { + require(isBlackListed[_blackListedUser]); + uint dirtyFunds = balanceOf(_blackListedUser); + balances[_blackListedUser] = 0; + _totalSupply -= dirtyFunds; + DestroyedBlackFunds(_blackListedUser, dirtyFunds); + } + + event DestroyedBlackFunds(address _blackListedUser, uint _balance); + + event AddedBlackList(address _user); + + event RemovedBlackList(address _user); + +} + +abstract contract UpgradedStandardToken is StandardToken{ + // those methods are called by the legacy contract + // and they must ensure msg.sender to be the contract address + function transferByLegacy(address from, address to, uint value) public virtual; + function transferFromByLegacy(address sender, address from, address spender, uint value) public virtual; + function approveByLegacy(address from, address spender, uint value) public virtual; +} + +contract TetherToken is Pausable, StandardToken, BlackList { + + using SafeMath for uint; + + string public name; + string public symbol; + uint public decimals; + address public upgradedAddress; + bool public deprecated; + + // The contract can be initialized with a number of tokens + // All the tokens are deposited to the owner address + // + // @param _balance Initial supply of the contract + // @param _name Token Name + // @param _symbol Token symbol + // @param _decimals Token decimals + constructor(uint _initialSupply, string memory _name, string memory _symbol, uint _decimals) public { + _totalSupply = _initialSupply; + name = _name; + symbol = _symbol; + decimals = _decimals; + balances[owner] = _initialSupply; + deprecated = false; + } + + // Forward ERC20 methods to upgraded contract if this one is deprecated + function transfer(address _to, uint _value) public override whenNotPaused { + require(!isBlackListed[msg.sender]); + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value); + } else { + return super.transfer(_to, _value); + } + } + + // Forward ERC20 methods to upgraded contract if this one is deprecated + function transferFrom(address _from, address _to, uint _value) public override whenNotPaused { + require(!isBlackListed[_from]); + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).transferFromByLegacy(msg.sender, _from, _to, _value); + } else { + return super.transferFrom(_from, _to, _value); + } + } + + // Forward ERC20 methods to upgraded contract if this one is deprecated + function balanceOf(address who) public override view returns (uint) { + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).balanceOf(who); + } else { + return super.balanceOf(who); + } + } + + // Forward ERC20 methods to upgraded contract if this one is deprecated + function approve(address _spender, uint _value) public override onlyPayloadSize(2 * 32) { + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).approveByLegacy(msg.sender, _spender, _value); + } else { + return super.approve(_spender, _value); + } + } + + // Forward ERC20 methods to upgraded contract if this one is deprecated + function allowance(address _owner, address _spender) public override view returns (uint remaining) { + if (deprecated) { + return StandardToken(upgradedAddress).allowance(_owner, _spender); + } else { + return super.allowance(_owner, _spender); + } + } + + // deprecate current contract in favour of a new one + function deprecate(address _upgradedAddress) public onlyOwner { + deprecated = true; + upgradedAddress = _upgradedAddress; + Deprecate(_upgradedAddress); + } + + // deprecate current contract if favour of a new one + function totalSupply() public override view returns (uint) { + if (deprecated) { + return StandardToken(upgradedAddress).totalSupply(); + } else { + return _totalSupply; + } + } + + // Issue a new amount of tokens + // these tokens are deposited into the owner address + // + // @param _amount Number of tokens to be issued + function issue(uint amount) public onlyOwner { + require(_totalSupply + amount > _totalSupply); + require(balances[owner] + amount > balances[owner]); + + balances[owner] += amount; + _totalSupply += amount; + Issue(amount); + } + + // Redeem tokens. + // These tokens are withdrawn from the owner address + // if the balance must be enough to cover the redeem + // or the call will fail. + // @param _amount Number of tokens to be issued + function redeem(uint amount) public onlyOwner { + require(_totalSupply >= amount); + require(balances[owner] >= amount); + + _totalSupply -= amount; + balances[owner] -= amount; + Redeem(amount); + } + + function setParams(uint newBasisPoints, uint newMaxFee) public onlyOwner { + // Ensure transparency by hardcoding limit beyond which fees can never be added + require(newBasisPoints < 20); + require(newMaxFee < 50); + + basisPointsRate = newBasisPoints; + maximumFee = newMaxFee.mul(10**decimals); + + Params(basisPointsRate, maximumFee); + } + + // Called when new token are issued + event Issue(uint amount); + + // Called when tokens are redeemed + event Redeem(uint amount); + + // Called when contract is deprecated + event Deprecate(address newAddress); + + // Called if contract ever adds fees + event Params(uint feeBasisPoints, uint maxFee); +} + +contract USD2_USDT is TetherToken { +// contract USD2 is TetherToken { + + string public constant NAME = "Tether USD - DUMMY"; + string public constant SYMBOL = "USDT"; + uint8 public constant DECIMALS = 6; + uint256 public constant INITIAL_SUPPLY = 10**24; + + constructor() + TetherToken( + INITIAL_SUPPLY, + NAME, + SYMBOL, + DECIMALS + ) + { } +} diff --git a/contracts/experiment/UsdtBuyer.sol b/contracts/experiment/UsdtBuyer.sol new file mode 100644 index 0000000..8884ea9 --- /dev/null +++ b/contracts/experiment/UsdtBuyer.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import {IUniswapV2Router} from "./IUniswapV2Router.sol"; + +contract UsdtBuyer { + + address public constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address private _owner; + + modifier onlyOwner() { + require(msg.sender == _owner, "not owner"); + _; + } + + constructor() { + _owner = msg.sender; + } + + function buy(uint256 amountIn, uint256 amountOutMin) + external + onlyOwner() + { + address[] memory path = new address[](2); + path[0] = IUniswapV2Router(UNISWAP_V2_ROUTER).WETH(); + path[1] = USDT; + + address to = msg.sender; + uint256 deadline = block.timestamp + 10 minutes; + IUniswapV2Router(UNISWAP_V2_ROUTER).swapExactETHForTokens{value: amountIn}( + amountOutMin, + path, + to, + deadline); + } + + // default function to transfer ethers to this contract + fallback() external payable {} + + // withdraw ethers from this contract + function withdraw(uint256 amount) external onlyOwner() { + payable(msg.sender).transfer(amount); + } +} \ No newline at end of file diff --git a/contracts/registry/IChainRegistryFacade.sol b/contracts/registry/IChainRegistryFacade.sol index dc3d626..593d5b8 100644 --- a/contracts/registry/IChainRegistryFacade.sol +++ b/contracts/registry/IChainRegistryFacade.sol @@ -30,6 +30,7 @@ interface IChainRegistryFacade { returns(uint96 nftId); function extendBundleLifetime(uint96 id, uint256 lifetimeExtension) external; + function setBundleExpiryAt(uint96 id, uint256 expiryAt) external; function owner() external view returns(address); function getNft() external view returns(IChainNftFacade); diff --git a/contracts/staking/IStakingFacadeExt.sol b/contracts/staking/IStakingFacadeExt.sol index ac370da..ec2a3ad 100644 --- a/contracts/staking/IStakingFacadeExt.sol +++ b/contracts/staking/IStakingFacadeExt.sol @@ -27,8 +27,12 @@ interface IStakingFacadeExt is IStakingFacade { uint40 createdAt; uint40 updatedAt; uint48 version; + uint40 lockedUntil; // introduced with V03 } - function getInfo(uint96 id) external view returns(StakeInfo memory info); + function setTargetRewardRate(uint96 targetNftId, uint256 newRewardRate) external; + function getTargetRewardRate(uint96 targetNftId) external view returns(uint256 rewardRate); + + function getInfo(uint96 stakeNftId) external view returns(StakeInfo memory info); function calculateRewardsIncrement(StakeInfo memory stakeInfo) external view returns(uint256 rewardsAmount); } \ No newline at end of file diff --git a/contracts/test/MockRegistryStaking.sol b/contracts/test/MockRegistryStaking.sol index 68b641b..512b13f 100644 --- a/contracts/test/MockRegistryStaking.sol +++ b/contracts/test/MockRegistryStaking.sol @@ -39,6 +39,7 @@ contract MockRegistryStaking is event LogMockComponentRegistered(uint256 id, bytes5 chain, uint8 objectType, bytes32 instanceId, uint256 riskpoolId, address to); event LogMockBundleRegistered(uint256 id, bytes5 chain, uint8 objectType, bytes32 instanceId, uint256 riskpoolId, uint256 bundleId, address to); event LogMockBundleLifetimeExtended(uint96 nftId, uint256 lifetimeExtension, address sender); + event LogMockBundleExpirySet(uint96 nftId, uint256 expiryAt, address sender); // keep track of chain and object specific minted counts, and items mapping(bytes5 /* chain id*/ => mapping(uint8 /* object type */ => uint96 [] /* nft ids*/)) private _objects; @@ -298,6 +299,34 @@ contract MockRegistryStaking is emit LogMockBundleLifetimeExtended(nftId, lifetimeExtension, msg.sender); } + function setBundleExpiryAt( + uint96 nftId, + uint256 expiryAtNew + ) + external + override + { + ( + bytes32 instanceId, + uint256 riskpoolId, + uint256 bundleId, + address token, + string memory displayName, + uint256 expiryAtOld + ) = decodeBundleData(nftId); + + _bundleData[nftId] = encodeBundleData( + instanceId, + riskpoolId, + bundleId, + token, + displayName, + expiryAtNew + ); + + emit LogMockBundleExpirySet(nftId, expiryAtNew, msg.sender); + } + function objects(bytes5 chain, uint8 objectType) external override view returns(uint256 numberOfObjects) { return _objects[chain][objectType].length; diff --git a/contracts/test/Tokens.sol b/contracts/test/Tokens.sol index 1360048..3eb4e76 100644 --- a/contracts/test/Tokens.sol +++ b/contracts/test/Tokens.sol @@ -44,6 +44,7 @@ contract USD1 is ERC20 { } +// contract USD2_Dummy is ERC20 { contract USD2 is ERC20 { // https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7 diff --git a/scripts/deploy_depeg.py b/scripts/deploy_depeg.py index 56be0b2..c56c528 100644 --- a/scripts/deploy_depeg.py +++ b/scripts/deploy_depeg.py @@ -104,6 +104,7 @@ def help(): print('# check mainnet setup') print("(setup, product, feeder, riskpool, registry, staking, dip, usdt, usdc, instance_service) = get_setup('0x8E43A861e9F270b58b1801171C627421Eb956cbA')") print('') + print("product_address = '0x8281f2bECfF37326Eb6cBE33527434611558B031'") print('(setup, product, feeder, riskpool, registry, staking, dip, usdt, usdc, instance_service) = get_setup(product_address)') print('') print('import json') @@ -278,7 +279,7 @@ def get_setup(product_address): (staking, registry, nft, dip_token) = (None, None, None, None) if riskpool.getStaking() != ZERO_ADDRESS: - staking = contract_from_address(interface.IStakingFacade, riskpool.getStaking()) + staking = contract_from_address(interface.IStakingFacadeExt, riskpool.getStaking()) staking_contract = (interface.IStakingFacade._name, str(staking)) staking_owner = staking.owner() dip_token = contract_from_address(DIP, staking.getDip()) @@ -473,6 +474,51 @@ def get_setup(product_address): ) +def filter_stakes(stakes, target_bundle=59101, min_balance=1): + total_rewards_open = 0 + total_rewards_balance = 0 + + print("# stake_nft_id target_nft_id stake_balance reward_balance rewards_open") + + for stake_nft_id in stakes.keys(): + stake = stakes[stake_nft_id] + if target_bundle and target_bundle != stake['target']: + continue + + if stakes[stake_nft_id]['stakeBalance'] < min_balance: + continue + + print(f"{stake_nft_id} {stake['target']} {stake['stakeBalance']} {stake['rewardBalance']} {stake['rewardsOpen']}") + total_rewards_open += stake['rewardsOpen'] + total_rewards_balance += stake['rewardBalance'] + + print(f"# total rewards balance {total_rewards_balance}") + print(f"# total rewards open {total_rewards_open}") + + +def get_stakes(registry, staking, chain_id=1, max_results=0) -> dict: + stakes = {} + stake_type_id = 10 + stakes_count = registry.objects(registry.toChain(chain_id), stake_type_id) + + if max_results > 0: + stakes_count = min(stakes_count, max_results) + + for i in range(stakes_count): + stake_nft_id = registry.getNftId(registry.toChain(chain_id), stake_type_id, i) + stake_info = staking.getInfo(stake_nft_id) + rewards_open = staking.calculateRewardsIncrement(stake_info) + + stake = stake_info.dict() + stake['rewardsOpen'] = rewards_open + + stakes[stake_nft_id] = stake + print(f'{i}/{stakes_count} {stake_nft_id}') + + return stakes + + + def _getStakeBalance(staking, dip): stake_balance = 0 diff --git a/scripts/setup.py b/scripts/setup.py index f7b2c8d..2824805 100644 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -87,6 +87,7 @@ def create_bundle( instanceService = instance.getInstanceService() token.transfer(investor, funding * tf, {'from': instanceOperator}) + token.approve(instanceService.getTreasuryAddress(), 0, {'from': investor}) token.approve(instanceService.getTreasuryAddress(), funding * tf, {'from': investor}) apr100level = riskpool.getApr100PercentLevel(); @@ -126,6 +127,7 @@ def apply_for_policy_with_bundle( if maxPremium > 0: token.transfer(customer, maxPremium * tf, {'from': instanceOperator}) + token.approve(instance.getTreasury(), 0, {'from': customer}) token.approve(instance.getTreasury(), maxPremium * tf, {'from': customer}) if not wallet: diff --git a/tests/conftest.py b/tests/conftest.py index 13b4727..6a27bbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -115,6 +115,10 @@ def protectedWallet2(accounts) -> Account: def registryOwner(accounts) -> Account: return get_filled_account(accounts, 13, "1 ether") +@pytest.fixture(scope="module") +def distributor(accounts) -> Account: + return get_filled_account(accounts, 14, "1 ether") + @pytest.fixture(scope="module") def theOutsider(accounts) -> Account: return get_filled_account(accounts, 19, "1 ether") diff --git a/tests/test_application_gasless.py b/tests/test_application_gasless.py index 57f58bf..fc711c6 100644 --- a/tests/test_application_gasless.py +++ b/tests/test_application_gasless.py @@ -190,6 +190,7 @@ def test_create_policy( # transfer some tokens to pay for premium premiumFunds = protectedBalance / 10 token.transfer(customer, premiumFunds, {'from': instanceOperator}) + token.approve(instanceService.getTreasuryAddress(), 0, {'from': customer}) token.approve(instanceService.getTreasuryAddress(), premiumFunds, {'from': customer}) # create application/policy for customer diff --git a/tests/test_bundle_registry_sync.py b/tests/test_bundle_registry_sync.py new file mode 100644 index 0000000..fd2db4e --- /dev/null +++ b/tests/test_bundle_registry_sync.py @@ -0,0 +1,79 @@ +import brownie +import pytest + +from brownie import MockRegistryStaking + +from scripts.setup import create_bundle + + +# enforce function isolation for tests below +@pytest.fixture(autouse=True) +def isolation(fn_isolation): + pass + + +def test_close_and_burn_bundle_sync_registry_expiry( + instance, + instanceService, + instanceOperator, + investor, + riskpool, + riskpoolKeeper, + riskpoolWallet, + dip, + usd2, + registryOwner, +): + mock = MockRegistryStaking.deploy(dip, usd2, {'from': registryOwner}) + mock.mockRegisterRiskpool(instanceService.getInstanceId(), riskpool.getId(), {'from': registryOwner}) + riskpool.setStakingAddress(mock, {'from': riskpoolKeeper}) + + bundle_id = create_bundle( + instance, + instanceOperator, + investor, + riskpool, + funding=100000, + bundleName='registry-sync-bundle', + bundleLifetimeDays=90, + minProtectedBalance=2000, + maxProtectedBalance=10000, + minDurationDays=14, + maxDurationDays=60, + aprPercentage=5.0, + ) + + bundle_nft = mock.getBundleNftId(instanceService.getInstanceId(), bundle_id) + registry_data_before = mock.decodeBundleData(bundle_nft).dict() + bundle_before = instanceService.getBundle(bundle_id).dict() + + assert registry_data_before['expiryAt'] == bundle_before['createdAt'] + 90 * 24 * 3600 + + brownie.chain.sleep(123) + brownie.chain.mine(1) + close_ts = brownie.chain.time() + + tx_close = riskpool.closeBundle(bundle_id, {'from': investor}) + assert 'LogRiskpoolBundleClosed' in tx_close.events + assert 'LogMockBundleExpirySet' in tx_close.events + + registry_data_closed = mock.decodeBundleData(bundle_nft).dict() + assert registry_data_closed['expiryAt'] == close_ts + + token = brownie.interface.IERC20Metadata(instanceService.getComponentToken(riskpool.getId())) + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpoolWallet}) + token.approve(instanceService.getTreasuryAddress(), 10**30, {'from': riskpoolWallet}) + + bundle_balance = instanceService.getBundle(bundle_id).dict()['balance'] + riskpool.defundBundle(bundle_id, bundle_balance, {'from': investor}) + + brownie.chain.sleep(77) + brownie.chain.mine(1) + burn_ts = brownie.chain.time() + + tx_burn = riskpool.burnBundle(bundle_id, {'from': investor}) + assert 'LogRiskpoolBundleBurned' in tx_burn.events + + registry_data_burned = mock.decodeBundleData(bundle_nft).dict() + assert registry_data_burned['expiryAt'] == close_ts + assert registry_data_burned['expiryAt'] < burn_ts diff --git a/tests/test_distribution.py b/tests/test_distribution.py new file mode 100644 index 0000000..06cdb4d --- /dev/null +++ b/tests/test_distribution.py @@ -0,0 +1,626 @@ +import brownie +import pytest + +from brownie.network.account import Account +from brownie import ( + chain, + history, + interface, + UsdcPriceDataProvider, + USD1, + USD2, + DIP, + DepegDistribution +) + +from scripts.util import ( + b2s, + contract_from_address +) +from scripts.depeg_product import ( + GifDepegProduct, + GifDepegRiskpool, +) + +from scripts.deploy_depeg import get_setup + +from scripts.price_data import ( + STATE_PRODUCT, + PERFECT_PRICE, + TRIGGER_PRICE, + # RECOVERY_PRICE, + inject_and_process_data, + generate_next_data, +) + +from scripts.setup import ( + create_bundle, + apply_for_policy_with_bundle, +) + +# enforce function isolation for tests below +@pytest.fixture(autouse=True) +def isolation(fn_isolation): + pass + +COMMISSION_RATE_DEFAULT = 0.05 +COMMISSION_RATE_MAX = 0.33 +COMMISSION_TOLERANCE = 10 ** -9 + +def test_deploy_distribution( + product20, + riskpool20, + productOwner, + distributor, + theOutsider, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + assert distribution.owner() == productOwner + assert distribution.getToken() == product20.getToken() + assert distribution.getToken() == usd2 + + assert distribution.COMMISSION_RATE_DEFAULT() / 10**distribution.DECIMALS() == COMMISSION_RATE_DEFAULT + assert distribution.COMMISSION_RATE_MAX() / 10 ** distribution.DECIMALS() == COMMISSION_RATE_MAX + + assert distribution.distributors() == 0 + assert not distribution.isDistributor(distributor) + assert distribution.getCommissionRate(distributor) == 0 + assert distribution.getCommissionBalance(distributor) == 0 + assert distribution.getPoliciesSold(distributor) == 0 + + assert not distribution.isDistributor(theOutsider) + + +def test_create_distributor_happy_case( + product20, + riskpool20, + productOwner, + distributor, + theOutsider, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + assert distribution.distributors() == 1 + assert distribution.isDistributor(distributor) + assert distribution.getCommissionRate(distributor) > 0 + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_DEFAULT() + assert distribution.getCommissionBalance(distributor) == 0 + assert distribution.getPoliciesSold(distributor) == 0 + + net_premium_100 = 100 * 10 ** usd2.decimals() + commission = distribution.calculateCommission(distributor, net_premium_100) + full_premium = net_premium_100 + commission + + commission_rate = distribution.getCommissionRate(distributor) + assert commission == full_premium * commission_rate / 10 ** distribution.DECIMALS() + + assert not distribution.isDistributor(theOutsider) + + +def test_set_commission_rate_happy_case( + product20, + productOwner, + distributor, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + # check initial setting + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_DEFAULT() + + # set to higher rate + commission_rate_new = 12 * 10 ** (distribution.DECIMALS() - 2); + distribution.setCommissionRate(distributor, commission_rate_new, {'from': productOwner}) + + assert commission_rate_new > distribution.COMMISSION_RATE_DEFAULT() + assert distribution.getCommissionRate(distributor) == commission_rate_new + + # set to max rate + distribution.setCommissionRate(distributor, distribution.COMMISSION_RATE_MAX(), {'from': productOwner}) + + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_MAX() + + # set rate to zero + commission_rate_zero = 0; + distribution.setCommissionRate(distributor, commission_rate_zero, {'from': productOwner}) + + assert distribution.getCommissionRate(distributor) == commission_rate_zero + + +def test_set_commission_rate_too_high( + product20, + productOwner, + distributor, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + # check initial setting + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_DEFAULT() + + # set to higher rate + commission_rate_too_high = distribution.COMMISSION_RATE_MAX() + 1 + with brownie.reverts("ERROR:DST-031:COMMISSION_RATE_TOO_HIGH"): + distribution.setCommissionRate(distributor, commission_rate_too_high, {'from': productOwner}) + + +def test_set_commission_rate_authz( + product20, + productOwner, + distributor, + theOutsider +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + # set to new rate + new_rate = 12 * 10 ** (distribution.DECIMALS() - 2); + + # attempt to set rate by distributor itself + with brownie.reverts("Ownable: caller is not the owner"): + distribution.setCommissionRate(distributor, new_rate, {'from': distributor}) + + # attempt to set rate by outsider + with brownie.reverts("Ownable: caller is not the owner"): + distribution.setCommissionRate(distributor, new_rate, {'from': theOutsider}) + + +def test_create_distributor_authz( + product20, + riskpool20, + productOwner, + distributor, + theOutsider, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + # attempt to self create distributor + with brownie.reverts('Ownable: caller is not the owner'): + distribution.createDistributor(distributor, {'from': distributor}) + + +def test_sell_policy_trough_distributor( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + tf = 10**usd2.decimals() + max_protected_balance = 10000 + bundle_funding = (max_protected_balance * 2) / 5 + bundle_id = create_bundle( + instance, + instanceOperator, + investor, + riskpool20, + maxProtectedBalance = max_protected_balance, + funding = bundle_funding) + + # setup up wallet to protect with some coins + protected_balance = 5000 * tf + usd1.transfer(protectedWallet, protected_balance, {'from': instanceOperator}) + + # buy policy for wallet to be protected + duration_days = 60 + max_premium = 100 + duration_seconds = duration_days * 24 * 3600 + + ( + total_premium, + commission + ) = distribution.calculatePrice( + distributor, + protected_balance, + duration_seconds, + bundle_id + ) + + # fund customer + usd2.transfer(customer, total_premium, {'from': instanceOperator}) + usd2.approve(distribution, total_premium, {'from': customer}) + + assert usd2.balanceOf(customer) == total_premium + assert usd2.balanceOf(distribution) == 0 + + # check distributor book keeping (before policy sale) + assert distribution.getCommissionBalance(distributor) == 0 + assert distribution.getPoliciesSold(distributor) == 0 + + # assert False + + tx = distribution.createPolicy( + customer, + protectedWallet, + protected_balance, + duration_seconds, + bundle_id, + {'from': distributor}) + + process_id = tx.events['LogApplicationCreated']['processId'] + + assert usd2.balanceOf(customer) == 0 + assert usd2.balanceOf(distribution) == commission + + # check owner of policy is distribution contract + # customer from above is only used to pull premium + meta_data = instanceService.getMetadata(process_id).dict() + assert meta_data['owner'] != customer + assert meta_data['owner'] == distribution + + # check distributor book keeping (after policy sale) + assert distribution.getCommissionBalance(distributor) == commission + assert distribution.getPoliciesSold(distributor) == 1 + + # check that all other policy properties match the direct sale setup + # see test_product_20.py::test_product_20_create_policy + protected_amount = protected_balance + sum_insured_amount = protected_amount / 5 + net_premium_amount = product20.calculateNetPremium(sum_insured_amount, duration_days * 24 * 3600, bundle_id) + premium_amount = product20.calculatePremium(net_premium_amount) + + # check application event data + events = history[-1].events + app_evt = dict(events['LogDepegApplicationCreated']) + assert app_evt['protectedBalance'] == protected_amount + assert app_evt['sumInsuredAmount'] == sum_insured_amount + assert app_evt['premiumAmount'] == premium_amount + + # check application data + application = instanceService.getApplication(process_id).dict() + application_data = riskpool20.decodeApplicationParameterFromData(application['data']).dict() + + assert application['sumInsuredAmount'] == riskpool20.calculateSumInsured(protected_amount) + assert application['sumInsuredAmount'] == sum_insured_amount + assert application_data['protectedBalance'] == protected_amount + + # check policy + policy = instanceService.getPolicy(process_id).dict() + + assert policy['premiumExpectedAmount'] == premium_amount + assert policy['premiumPaidAmount'] == premium_amount + assert policy['payoutMaxAmount'] == sum_insured_amount + assert policy['payoutAmount'] == 0 + + # check bundle data + bundle = instanceService.getBundle(bundle_id).dict() + funding_amount = bundle_funding * tf + + assert bundle['balance'] == funding_amount + net_premium_amount + assert bundle['capital'] == funding_amount + assert bundle['lockedCapital'] == sum_insured_amount + + # check riskpool numbers + assert riskpool20.getBalance() == bundle['balance'] + assert riskpool20.getTotalValueLocked() == sum_insured_amount + assert riskpool20.getCapacity() == funding_amount - sum_insured_amount + + # check riskpool wallet + assert usd2.balanceOf(riskpoolWallet) == riskpool20.getBalance() + + +def test_withdrawal_distributor_happy_case( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + ( + distribution, + commission + ) = _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1, + usd2 + ) + + assert usd2.balanceOf(distributor) == 0 + assert usd2.balanceOf(distribution) == commission + + withdrawal_amount = 100000 + remaining_commission = commission - withdrawal_amount + tx = distribution.withdrawCommission(withdrawal_amount, {'from': distributor}) + + # check updated book keeping + assert distribution.getCommissionBalance(distributor) == remaining_commission + + # check actual token balances + assert usd2.balanceOf(distributor) == withdrawal_amount + assert usd2.balanceOf(distribution) == remaining_commission + + +def test_withdrawal_distributor_amount_too_big( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + ( + distribution, + commission + ) = _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1, + usd2 + ) + + assert usd2.balanceOf(distributor) == 0 + assert usd2.balanceOf(distribution) == commission + + # amount larger than accumulated commission + with brownie.reverts("ERROR:DST-050:AMOUNT_TOO_LARGE"): + distribution.withdrawCommission(commission + 1, {'from': distributor}) + + # reduce commission balance of distribution contract + distribution.withdraw(commission - 1000, {'from': productOwner}) + + # amount smaller accumulated commission + with brownie.reverts("ERROR:DST-051:BALANCE_INSUFFICIENT"): + distribution.withdrawCommission(commission - 1, {'from': distributor}) + + +def test_withdrawal_not_distributor( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + theOutsider, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + ( + distribution, + commission + ) = _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1, + usd2 + ) + + assert usd2.balanceOf(distributor) == 0 + assert usd2.balanceOf(distribution) == commission + + with brownie.reverts("ERROR:DST-001:NOT_DISTRIBUTOR"): + distribution.withdrawCommission(commission + 1, {'from': productOwner}) + + with brownie.reverts("ERROR:DST-001:NOT_DISTRIBUTOR"): + distribution.withdrawCommission(commission + 1, {'from': theOutsider}) + + # now, make the outsider to distributor - but not the one that owns the one with the commission + distribution.createDistributor(theOutsider, {'from': productOwner}) + + # amount larger than accumulated commission + with brownie.reverts("ERROR:DST-050:AMOUNT_TOO_LARGE"): + distribution.withdrawCommission(commission + 1, {'from': theOutsider}) + + +def test_withdrawal_owner_happy_case( + productOwner, + instanceOperator, + product20, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + some_amount = 1000 * 10 ** usd2.decimals() + usd2.transfer(distribution, some_amount, {'from': instanceOperator}) + + # check balances before withdrawal + assert usd2.balanceOf(distribution) == some_amount + assert usd2.balanceOf(productOwner) == 0 + + other_amount = 200 * 10 ** usd2.decimals() + distribution.withdraw(other_amount, {'from': productOwner}) + + # check balances after withdrawal + assert usd2.balanceOf(distribution) == some_amount - other_amount + assert usd2.balanceOf(productOwner) == other_amount + + +def test_withdrawal_non_owner( + productOwner, + instanceOperator, + distributor, + theOutsider, + product20, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + some_amount = 1000 * 10 ** usd2.decimals() + usd2.transfer(distribution, some_amount, {'from': instanceOperator}) + + # check balances before withdrawal + assert usd2.balanceOf(distribution) == some_amount + assert usd2.balanceOf(productOwner) == 0 + + other_amount = 200 * 10 ** usd2.decimals() + + # attempt withdrawal by outsider + with brownie.reverts("Ownable: caller is not the owner"): + distribution.withdraw(other_amount, {'from': theOutsider}) + + # attempt withdrawal by distributor + with brownie.reverts("Ownable: caller is not the owner"): + distribution.withdraw(other_amount, {'from': distributor}) + + +def test_withdrawal_amount_too_big( + productOwner, + instanceOperator, + product20, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + some_amount = 1000 * 10 ** usd2.decimals() + usd2.transfer(distribution, some_amount, {'from': instanceOperator}) + + # check balances before withdrawal + assert usd2.balanceOf(distribution) == some_amount + assert usd2.balanceOf(productOwner) == 0 + + # amount larger than balance + other_amount = some_amount + 1 + + # attempt withdrawal by too large amount + with brownie.reverts("ERROR:DST-040:BALANCE_INSUFFICIENT"): + distribution.withdraw(other_amount, {'from': productOwner}) + + +def _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + tf = 10**usd2.decimals() + max_protected_balance = 10000 + bundle_funding = (max_protected_balance * 2) / 5 + bundle_id = create_bundle( + instance, + instanceOperator, + investor, + riskpool20, + maxProtectedBalance = max_protected_balance, + funding = bundle_funding) + + # setup up wallet to protect with some coins + protected_balance = 5000 * tf + usd1.transfer(protectedWallet, protected_balance, {'from': instanceOperator}) + + # buy policy for wallet to be protected + duration_days = 60 + duration_seconds = duration_days * 24 * 3600 + + ( + total_premium, + commission + ) = distribution.calculatePrice( + distributor, + protected_balance, + duration_seconds, + bundle_id + ) + + # fund customer + usd2.transfer(customer, total_premium, {'from': instanceOperator}) + usd2.approve(distribution, total_premium, {'from': customer}) + + tx = distribution.createPolicy( + customer, + protectedWallet, + protected_balance, + duration_seconds, + bundle_id, + {'from': distributor}) + + return ( + distribution, + commission + ) + + +def _deploy_distribution( + product, + productOwner, +): + return DepegDistribution.deploy( + product, + product.getId(), + {'from': productOwner}) diff --git a/tests/test_policy_lifecycle.py b/tests/test_policy_lifecycle.py index 52840dd..bdae1dd 100644 --- a/tests/test_policy_lifecycle.py +++ b/tests/test_policy_lifecycle.py @@ -65,6 +65,7 @@ def test_happy_path( # create token allowance for payouts max_payout_amount = 100000 + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpool_wallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, @@ -520,6 +521,7 @@ def test_close_policy_no_depeg_event( # create token allowance for payouts max_payout_amount = 100000 + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpool_wallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, @@ -617,6 +619,7 @@ def test_close_policy_after_depeg_event_well_before_expiry( # create token allowance for payouts max_payout_amount = 100000 + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpool_wallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, @@ -741,6 +744,7 @@ def test_close_policy_after_depeg_event_at_end_of_expiry( # create token allowance for payouts max_payout_amount = 100000 + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpool_wallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, @@ -863,6 +867,7 @@ def test_over_protected_with_single_policy( # create token allowance for payouts max_payout_amount = 100000 + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpool_wallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, @@ -992,6 +997,7 @@ def test_over_protected_with_multiple_policies( # create token allowance for payouts max_payout_amount = 100000 + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpool_wallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, diff --git a/tests/test_product_20.py b/tests/test_product_20.py index 8a9c6e7..4999e0f 100644 --- a/tests/test_product_20.py +++ b/tests/test_product_20.py @@ -232,6 +232,7 @@ def test_product_20_try_to_create_policy_for_locked_bundle( max_premium = 100 usd2.transfer(customer, max_premium * tf, {'from': instanceOperator}) + usd2.approve(instanceService.getTreasuryAddress(), 0, {'from': customer}) usd2.approve(instanceService.getTreasuryAddress(), max_premium * tf, {'from': customer}) # lock bundle @@ -330,6 +331,7 @@ def test_premium_payment( # setup with correct balance and allowance usd2.transfer(theOutsider, premium, {'from': instanceOperator}) + usd2.approve(instance.getTreasury(), 0, {'from': theOutsider}) usd2.approve(instance.getTreasury(), premium, {'from': theOutsider}) # check actual account balances before @@ -400,6 +402,7 @@ def test_create_policy_bad_balance_or_allowance( # failure case 1: balance too small, allowance ok usd2.transfer(theOutsider, premium - missing_from_balance, {'from': instanceOperator}) + usd2.approve(instance.getTreasury(), 0, {'from': theOutsider}) usd2.approve(instance.getTreasury(), premium, {'from': theOutsider}) with brownie.reverts('ERROR:DP-014:BALANCE_TOO_LOW'): @@ -411,6 +414,7 @@ def test_create_policy_bad_balance_or_allowance( {'from': theOutsider}) # failure case 2: balance and allowance too small + usd2.approve(instance.getTreasury(), 0, {'from': theOutsider}) usd2.approve(instance.getTreasury(), premium - missing_from_allowance, {'from': theOutsider}) with brownie.reverts('ERROR:DP-014:BALANCE_TOO_LOW'): @@ -433,6 +437,7 @@ def test_create_policy_bad_balance_or_allowance( {'from': theOutsider}) # ok case 1: balance ok,, allowance ok + usd2.approve(instance.getTreasury(), 0, {'from': theOutsider}) usd2.approve(instance.getTreasury(), premium, {'from': theOutsider}) tx = product20.applyForPolicyWithBundle( @@ -486,6 +491,7 @@ def test_product_20_depeg_normal( # create token allowance for payouts max_protected_balance = 10000 max_payout_amount = max_protected_balance + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpoolWallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, @@ -657,6 +663,7 @@ def test_product_20_depeg_below_80( # create token allowance for payouts max_protected_balance = 10000 max_payout_amount = max_protected_balance + token.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpoolWallet}) token.approve( instanceService.getTreasuryAddress(), max_payout_amount * tf, diff --git a/tests/test_riskpool_capping.py b/tests/test_riskpool_capping.py index 38daed6..2c2a80a 100644 --- a/tests/test_riskpool_capping.py +++ b/tests/test_riskpool_capping.py @@ -170,6 +170,7 @@ def test_riskpool_enforcing_caps_simple( # attempt to increase bundle capital via bundle funding increase_amount = 1 + usd2.approve(instanceService.getTreasuryAddress(), 0, {'from': investor}) usd2.approve(instanceService.getTreasuryAddress(), increase_amount, {'from': investor}) with brownie.reverts('ERROR:DRP-100:FUNDING_EXCEEDS_BUNDLE_CAPITAL_CAP'): @@ -181,6 +182,7 @@ def test_riskpool_enforcing_caps_simple( # check that defunding, then funding again works delta_amount = 10 * 10 ** usd2.decimals() + usd2.approve(instanceService.getTreasuryAddress(), 0, {'from': riskpoolWallet}) usd2.approve(instanceService.getTreasuryAddress(), delta_amount, {'from': riskpoolWallet}) riskpool.defundBundle( bundle_id, @@ -189,6 +191,7 @@ def test_riskpool_enforcing_caps_simple( assert instanceService.getBundle(bundle_id).dict()['capital'] == bundle_cap * tf - delta_amount + usd2.approve(instanceService.getTreasuryAddress(), 0, {'from': investor}) usd2.approve(instanceService.getTreasuryAddress(), delta_amount, {'from': investor}) riskpool.fundBundle( bundle_id, @@ -256,6 +259,7 @@ def test_riskpool_enforcing_caps_multiple_bundles( # verify that funding bundles is not possible even with its capital < bundle_cap increase_amount = 1 + usd2.approve(instanceService.getTreasuryAddress(), 0, {'from': investor}) usd2.approve(instanceService.getTreasuryAddress(), increase_amount, {'from': investor}) # try to fund bundl 1