├── .gitattributes ├── .gitignore ├── .vscode └── settings.json ├── brownie-config.yaml ├── contracts └── DaycareManager.sol ├── flattened_manager.sol ├── interfaces └── IAdventureTime.sol ├── readme.md ├── requirements.txt ├── scripts └── deploy.py └── tests ├── conftest.py ├── test_execute_daycare.py └── test_register_daycare.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | 7 | .env 8 | env/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | networks: 2 | default: ftm-main 3 | # default: ftm-main-fork 4 | 5 | dotenv: .env 6 | 7 | autofetch_sources: true -------------------------------------------------------------------------------- /contracts/DaycareManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "../interfaces/IAdventureTime.sol"; 5 | 6 | contract DaycareManager { 7 | IAdventureTime adventureTime = 8 | IAdventureTime(0x0D4C98901563ca730332e841EDBCB801fe9F2551); 9 | uint256 public constant DAILY_FEE = 0.1 * 1e18; 10 | 11 | mapping(uint256 => uint256) public daysPaid; 12 | 13 | event registeredDaycare( 14 | address _registerer, 15 | uint256 _summonerId, 16 | uint256 _days 17 | ); 18 | event executedDaycare(address _executor, uint256 _summonerId); 19 | 20 | function registerDaycare( 21 | uint256[] calldata _summonerIds, 22 | uint256[] calldata _days 23 | ) external payable { 24 | uint256 len = _summonerIds.length; 25 | require(len == _days.length, "DCM: Invalid lengths"); 26 | uint256 totalFee = 0; 27 | for (uint256 i = 0; i < len; i++) { 28 | require(_days[i] > 0, "DCM: Cannot daycare for 0 days"); 29 | daysPaid[_summonerIds[i]] += _days[i]; 30 | totalFee += _days[i] * DAILY_FEE; 31 | emit registeredDaycare(msg.sender, _summonerIds[i], _days[i]); 32 | } 33 | require(msg.value >= totalFee, "DCM: Insufficient fee"); 34 | // Don't send too much FTM, otherwise it will be stuck in the contract 35 | } 36 | 37 | function executeDaycare(uint256[] calldata _summonerIds) external { 38 | for (uint256 i = 0; i < _summonerIds.length; i++) { 39 | daysPaid[_summonerIds[i]] -= 1; 40 | emit executedDaycare(msg.sender, _summonerIds[i]); 41 | } 42 | // Below line will revert if any summoners can't be adventured 43 | adventureTime.adventureTime(_summonerIds); 44 | payable(msg.sender).transfer(_summonerIds.length * DAILY_FEE); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /flattened_manager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | interface IAdventureTime { 5 | function adventureTime(uint256[] calldata _summoner) external; 6 | } 7 | 8 | contract DaycareManager { 9 | IAdventureTime adventureTime = 10 | IAdventureTime(0x0D4C98901563ca730332e841EDBCB801fe9F2551); 11 | uint256 public constant DAILY_FEE = 0.1 * 1e18; 12 | 13 | mapping(uint256 => uint256) public daysPaid; 14 | 15 | event registeredDaycare( 16 | address _registerer, 17 | uint256 _summonerId, 18 | uint256 _days 19 | ); 20 | event executedDaycare(address _executor, uint256 _summonerId); 21 | 22 | function registerDaycare( 23 | uint256[] calldata _summonerIds, 24 | uint256[] calldata _days 25 | ) external payable { 26 | uint256 len = _summonerIds.length; 27 | require(len == _days.length, "DCM: Invalid lengths"); 28 | uint256 totalFee = 0; 29 | for (uint256 i = 0; i < len; i++) { 30 | require(_days[i] > 0, "DCM: Cannot daycare for 0 days"); 31 | daysPaid[_summonerIds[i]] += _days[i]; 32 | totalFee += _days[i] * DAILY_FEE; 33 | emit registeredDaycare(msg.sender, _summonerIds[i], _days[i]); 34 | } 35 | require(msg.value >= totalFee, "DCM: Insufficient fee"); 36 | // Don't send too much FTM, otherwise it will be stuck in the contract 37 | } 38 | 39 | function executeDaycare(uint256[] calldata _summonerIds) external { 40 | for (uint256 i = 0; i < _summonerIds.length; i++) { 41 | daysPaid[_summonerIds[i]] -= 1; 42 | emit executedDaycare(msg.sender, _summonerIds[i]); 43 | } 44 | // Below line will revert if any summoners can't be adventured 45 | adventureTime.adventureTime(_summonerIds); 46 | payable(msg.sender).transfer(_summonerIds.length * DAILY_FEE); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /interfaces/IAdventureTime.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | interface IAdventureTime { 5 | function adventureTime(uint[] calldata _summoner) external; 6 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Daycare Manager 2 | 3 | Deployed at [0xf1bf34E46ECf465591B7a7fA9635E4C583174fa3](https://ftmscan.com/address/0xf1bf34e46ecf465591b7a7fa9635e4c583174fa3) 4 | 5 | Framework to allow users to create permissionless bots to adventure time 6 | You can register your summoners with `registerDaycare()` and will pay a fee of 0.1 FTM per summoner per day. 7 | 8 | You will need to approve the adventure time contract `0x0D4C98901563ca730332e841EDBCB801fe9F2551` in order to allow users to adventure on your behalf 9 | 10 | Anybody will be able to adventure on your behalf in a trustless manner, and in doing so, will receive the 0.1 FTM fee. 11 | 12 | Advanced coders will be able to generate scripts/bots that can adventure on behalf of users by calling `executeDaycare()` 13 | Hopefully the economic incentives add up. 14 | 15 | Feel free to deploy to re-deploy the contract with a different fee if the price of FTM varies significantly. 16 | 17 | 18 | ## Guide 19 | 20 | 1. Go to [rarity contract](https://ftmscan.com/address/0xce761d788df608bd21bdd59d6f4b54b2e27f25bb#writeContract) and approve the adventure time contract to adventure() with your summoner: 21 | Under `approve` enter: 22 | - to: `0x0D4C98901563ca730332e841EDBCB801fe9F2551` 23 | - tokenId: the token ID for your summoner 24 | 25 | 2. Calculate the cost to enter day care. This will be 0.1 FTM per day. For example, for 7 days, this will be 0.7 FTM. 26 | 27 | 3. Go to [daycare manager](https://ftmscan.com/address/0xf1bf34e46ecf465591b7a7fa9635e4c583174fa3#writeContract) and register your summoner for day care 28 | Under `registerDaycare` enter: 29 | - payableAmount: Enter the calculated fee here ie 0.7 30 | - summonerId: Array of tokenIDs. For a single token, enclose this in square brackets eg. [110775] 31 | - days: Array of days. With the above example, you will need to enter [7] 32 | 33 | 34 | 35 | ## Notes 36 | - You cannot unregister once you have registered -> I would recommend only registering small number of days at a time 37 | - This isn't audited. 38 | - `adventureTime.adventureTime(_summonerIds)` will revert if ANY of the summoner ids is invalid. 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie==1.16.0 -------------------------------------------------------------------------------- /scripts/deploy.py: -------------------------------------------------------------------------------- 1 | import click 2 | from brownie import accounts, DaycareManager, network 3 | 4 | 5 | def main(): 6 | print(f"Deploying to '{network.show_active()}' network") 7 | acct = accounts.load(click.prompt("account", type=click.Choice(accounts.load()))) 8 | DaycareManager.deploy({"from": acct}, publish_source=True) 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import Contract 3 | 4 | 5 | @pytest.fixture(autouse=True) 6 | def isolation(fn_isolation): 7 | pass 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def deployer(accounts): 12 | yield accounts[0] 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def alice(accounts): 17 | yield accounts[1] 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def bob(accounts): 22 | yield accounts[2] 23 | 24 | 25 | @pytest.fixture(scope="module") 26 | def keeper(accounts): 27 | yield accounts[3] 28 | 29 | 30 | @pytest.fixture(scope="module") 31 | def dcm(deployer, DaycareManager): 32 | yield deployer.deploy(DaycareManager) 33 | 34 | 35 | @pytest.fixture(scope="module") 36 | def adventuretime(): 37 | yield Contract("0x0D4C98901563ca730332e841EDBCB801fe9F2551") 38 | 39 | 40 | @pytest.fixture(scope="module") 41 | def rarity(): 42 | yield Contract("0xce761D788DF608BD21bdd59d6f4B54b2e27F25Bb") 43 | -------------------------------------------------------------------------------- /tests/test_execute_daycare.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie.test import given, strategy 4 | from brownie import chain 5 | import math 6 | 7 | # EXECUTE DAYCARE 8 | 9 | fee_per_execution = 1e17 10 | 11 | 12 | @given(summoner_id=strategy("uint256")) 13 | def test_execute_without_setup(dcm, summoner_id, keeper): 14 | with brownie.reverts(): 15 | dcm.executeDaycare([summoner_id], {"from": keeper}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def alice_summoned(alice, adventuretime, rarity): 20 | tx = rarity.summon(1, {"from": alice}) 21 | summoner_id = tx.events["summoned"]["summoner"] 22 | rarity.approve(adventuretime.address, summoner_id, {"from": alice}) 23 | yield summoner_id 24 | 25 | 26 | def test_execute_with_setup(dcm, alice, keeper, alice_summoned, rarity): 27 | balance = keeper.balance() 28 | dcm.registerDaycare( 29 | [alice_summoned], [1], {"from": alice, "value": fee_per_execution} 30 | ) 31 | dcm.executeDaycare([alice_summoned], {"from": keeper}) 32 | assert rarity.xp(alice_summoned) == 250 * 1e18 33 | assert balance + fee_per_execution == keeper.balance() 34 | 35 | 36 | def test_execute_twice_no_break(dcm, alice, keeper, alice_summoned): 37 | dcm.registerDaycare( 38 | [alice_summoned], [2], {"from": alice, "value": 2 * fee_per_execution} 39 | ) 40 | dcm.executeDaycare([alice_summoned], {"from": keeper}) 41 | with brownie.reverts(): 42 | dcm.executeDaycare([alice_summoned], {"from": keeper}) 43 | 44 | 45 | def test_execute_twice_with_break(dcm, alice, keeper, alice_summoned, rarity): 46 | balance = keeper.balance() 47 | dcm.registerDaycare( 48 | [alice_summoned], [2], {"from": alice, "value": 2 * fee_per_execution} 49 | ) 50 | dcm.executeDaycare([alice_summoned], {"from": keeper}) 51 | chain.sleep(math.floor(1 * 60 * 60 * 24.1)) 52 | chain.mine() 53 | dcm.executeDaycare([alice_summoned], {"from": keeper}) 54 | assert balance + 2 * fee_per_execution == keeper.balance() 55 | assert rarity.xp(alice_summoned) == 500 * 1e18 56 | -------------------------------------------------------------------------------- /tests/test_register_daycare.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie.test import given, strategy 3 | 4 | # REGISTER DAYCARE 5 | 6 | fee_per_execution = 1e17 7 | 8 | 9 | @given( 10 | summoner_id=strategy("uint256"), 11 | days=strategy("uint256", min_value=1, max_value=365), 12 | ) 13 | def test_add_single_summoner(dcm, alice, summoner_id, days): 14 | if days * fee_per_execution < 100e18: 15 | dcm.registerDaycare( 16 | [summoner_id], [days], {"from": alice, "value": days * fee_per_execution} 17 | ) 18 | assert dcm.daysPaid(summoner_id) == days 19 | 20 | 21 | @given( 22 | summoner_idA=strategy("uint256"), 23 | summoner_idB=strategy("uint256"), 24 | daysA=strategy("uint256", min_value=1, max_value=365), 25 | daysB=strategy("uint256", min_value=1, max_value=365), 26 | ) 27 | def test_add_two_summoners(dcm, alice, summoner_idA, summoner_idB, daysA, daysB): 28 | fee = (daysA + daysB) * fee_per_execution 29 | if fee < 100e18: 30 | dcm.registerDaycare( 31 | [summoner_idA, summoner_idB], [daysA, daysB], {"from": alice, "value": fee} 32 | ) 33 | if summoner_idA == summoner_idB: 34 | assert dcm.daysPaid(summoner_idA) == daysA + daysB 35 | else: 36 | assert dcm.daysPaid(summoner_idA) == daysA 37 | assert dcm.daysPaid(summoner_idB) == daysB 38 | 39 | 40 | @given( 41 | summoner_id=strategy("uint256"), 42 | days=strategy("uint256", min_value=1, max_value=365), 43 | ) 44 | def test_insufficient_fee(dcm, alice, summoner_id, days): 45 | insufficient_amount = days * 1e16 46 | with brownie.reverts("DCM: Insufficient fee"): 47 | dcm.registerDaycare( 48 | [summoner_id], [days], {"from": alice, "value": insufficient_amount} 49 | ) 50 | print(insufficient_amount, days * fee_per_execution) 51 | 52 | 53 | @given( 54 | summoner_id=strategy("uint256"), 55 | days=strategy("uint256", min_value=1, max_value=365), 56 | ) 57 | def test_invalid_length(dcm, alice, summoner_id, days): 58 | with brownie.reverts("DCM: Invalid lengths"): 59 | dcm.registerDaycare( 60 | [summoner_id, summoner_id], 61 | [days], 62 | {"from": alice, "value": days * fee_per_execution}, 63 | ) 64 | 65 | 66 | @given(summoner_id=strategy("uint256")) 67 | def test_zero_days(dcm, alice, summoner_id): 68 | with brownie.reverts("DCM: Cannot daycare for 0 days"): 69 | dcm.registerDaycare([summoner_id], [0], {"from": alice}) 70 | --------------------------------------------------------------------------------