diff --git a/contracts/arbitrum/connectors/arb-claim/events.sol b/contracts/arbitrum/connectors/arb-claim/events.sol new file mode 100644 index 00000000..6d220d43 --- /dev/null +++ b/contracts/arbitrum/connectors/arb-claim/events.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +contract Events { + event LogArbAirdropClaimed( + address indexed account, + uint256 indexed claimable, + uint256 setId + ); + + event LogArbTokensDelegated( + address indexed account, + address indexed delegatee + ); +} diff --git a/contracts/arbitrum/connectors/arb-claim/helpers.sol b/contracts/arbitrum/connectors/arb-claim/helpers.sol new file mode 100644 index 00000000..4eb872de --- /dev/null +++ b/contracts/arbitrum/connectors/arb-claim/helpers.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import "./variables.sol"; +import { Basic } from "../../common/basic.sol"; + +contract Helpers is Variables, Basic { + function claimableArbTokens(address user) public view returns (uint256) { + return ARBITRUM_TOKEN_DISTRIBUTOR.claimableTokens(user); + } +} \ No newline at end of file diff --git a/contracts/arbitrum/connectors/arb-claim/interface.sol b/contracts/arbitrum/connectors/arb-claim/interface.sol new file mode 100644 index 00000000..3831a6cf --- /dev/null +++ b/contracts/arbitrum/connectors/arb-claim/interface.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +interface IArbitrumTokenDistributor { + function claim() external; + + function claimAndDelegate( + address delegatee, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function claimableTokens(address) external view returns (uint256); +} + +interface IArbTokenContract { + function delegate(address delegatee) external; + + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} + +interface TokenInterface { + function approve(address, uint256) external; + + function transfer(address, uint256) external; + + function transferFrom( + address, + address, + uint256 + ) external; + + function deposit() external payable; + + function withdraw(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function decimals() external view returns (uint256); +} diff --git a/contracts/arbitrum/connectors/arb-claim/main.sol b/contracts/arbitrum/connectors/arb-claim/main.sol new file mode 100644 index 00000000..f908b8b1 --- /dev/null +++ b/contracts/arbitrum/connectors/arb-claim/main.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "./helpers.sol"; +import { Events } from "./events.sol"; +import { IArbitrumTokenDistributor } from "./interface.sol"; + +abstract contract ArbitrumAirdrop is Events, Helpers { + + /** + * @dev DSA Arbitrum airdrop claim function. + * @param setId ID to set the claimable amount in the DSA + */ + function claimAirdrop(uint256 setId) + public + returns (string memory eventName_, bytes memory eventParam_) + { + uint256 claimable = claimableArbTokens(address(this)); + + // If claimable <= 0, ARB TokenDistributor will revert on claim. + ARBITRUM_TOKEN_DISTRIBUTOR.claim(); + setUint(setId, claimable); + + eventName_ = "LogArbAirdropClaimed(address,uint256,uint256)"; + eventParam_ = abi.encode(address(this), claimable, setId); + } + + /** + * @dev Delegates votes from signer to `delegatee`. + * @param delegatee The address to delegate the ARB tokens to. + */ + function delegate(address delegatee) + public + returns (string memory eventName_, bytes memory eventParam_) + { + ARB_TOKEN_CONTRACT.delegate(delegatee); + + eventName_ = "LogArbTokensDelegated(address,address)"; + eventParam_ = abi.encode(address(this), delegatee); + } +} + +contract ConnectV2ArbitrumAirdrop is ArbitrumAirdrop { + string public constant name = "ArbitrumAirdrop-v1"; +} diff --git a/contracts/arbitrum/connectors/arb-claim/variables.sol b/contracts/arbitrum/connectors/arb-claim/variables.sol new file mode 100644 index 00000000..fb3e29d5 --- /dev/null +++ b/contracts/arbitrum/connectors/arb-claim/variables.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import "./interface.sol"; + +contract Variables { + IArbitrumTokenDistributor public constant ARBITRUM_TOKEN_DISTRIBUTOR = + IArbitrumTokenDistributor(0x67a24CE4321aB3aF51c2D0a4801c3E111D88C9d9); + + IArbTokenContract public constant ARB_TOKEN_CONTRACT = + IArbTokenContract(0x912CE59144191C1204E64559FE8253a0e49E6548); +} diff --git a/test/arbitrum/arb-claim/test.ts b/test/arbitrum/arb-claim/test.ts new file mode 100644 index 00000000..734fc065 --- /dev/null +++ b/test/arbitrum/arb-claim/test.ts @@ -0,0 +1,80 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { ethers } from "hardhat"; +import { ConnectV2ArbitrumAirdrop, ConnectV2ArbitrumAirdrop__factory } from "../../../typechain"; +import hre from "hardhat"; +import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; +import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; +import { encodeSpells } from "../../../scripts/tests/encodeSpells"; +import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; +import { addresses } from "../../../scripts/tests/arbitrum/addresses"; +import { abis } from "../../../scripts/constant/abis"; + +describe("Arbitrum Airdrop Claim Test", () => { + let signer: SignerWithAddress; + let signer_user: any; + const user = "0x30c3D961a21c2352A6FfAfFd4e8cB8730Bf82757"; + const connectorName = "arbitrum-airdrop"; + let dsaWallet0: any; + + before(async () => { + [signer] = await ethers.getSigners(); + }); + + describe("Arbitrum Airdrop Functions", () => { + let contract: ConnectV2ArbitrumAirdrop; + + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + //@ts-ignore + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 70606643, + }, + }, + ], + }); + + const deployer = new ConnectV2ArbitrumAirdrop__factory(signer); + contract = await deployer.deploy(); + await contract.deployed(); + console.log("Contract deployed at: ", contract.address); + + await deployAndEnableConnector({ + connectorName, + contractArtifact: ConnectV2ArbitrumAirdrop__factory, + signer: signer, + connectors: await ethers.getContractAt(abis.core.connectorsV2, addresses.core.connectorsV2), + }); + + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [user], + }); + + signer_user = await ethers.getSigner(user); + dsaWallet0 = await buildDSAv2(user); + }); + + it("Claims Arbitrum Airdrop and checks claimable tokens", async () => { + const claimableBefore = await contract.claimableArbTokens(user); + console.log("Claimable tokens before: ", claimableBefore.toString()); + + const spells = [ + { + connector: connectorName, + method: "claimAirdrop", + args: ["0"], + }, + ]; + + const tx = await dsaWallet0.connect(signer_user).cast(...encodeSpells(spells), user); + await tx.wait(); + + const claimableAfter = await contract.claimableArbTokens(user); + console.log("Claimable tokens after: ", claimableAfter.toString()); + }); + }); +}); \ No newline at end of file