diff --git a/contracts/src/libraries/token.cairo b/contracts/src/libraries/token.cairo index b6b4d8df3..4260d356f 100644 --- a/contracts/src/libraries/token.cairo +++ b/contracts/src/libraries/token.cairo @@ -1 +1,2 @@ -mod erc677; +mod v1; +mod v2; diff --git a/contracts/src/libraries/token/v1.cairo b/contracts/src/libraries/token/v1.cairo new file mode 100644 index 000000000..b6b4d8df3 --- /dev/null +++ b/contracts/src/libraries/token/v1.cairo @@ -0,0 +1 @@ +mod erc677; diff --git a/contracts/src/libraries/token/erc677.cairo b/contracts/src/libraries/token/v1/erc677.cairo similarity index 100% rename from contracts/src/libraries/token/erc677.cairo rename to contracts/src/libraries/token/v1/erc677.cairo diff --git a/contracts/src/libraries/token/v2.cairo b/contracts/src/libraries/token/v2.cairo new file mode 100644 index 000000000..cafd74014 --- /dev/null +++ b/contracts/src/libraries/token/v2.cairo @@ -0,0 +1,2 @@ +mod erc677; +mod erc677_receiver; diff --git a/contracts/src/libraries/token/v2/erc677.cairo b/contracts/src/libraries/token/v2/erc677.cairo new file mode 100644 index 000000000..aa86d1c5f --- /dev/null +++ b/contracts/src/libraries/token/v2/erc677.cairo @@ -0,0 +1,79 @@ +use starknet::ContractAddress; + +const IERC677_ID: felt252 = 0x3c4538abc63e0cdf912cef3d2e1389d0b2c3f24ee0c06b21736229f52ece6c8; + +#[starknet::interface] +trait IERC677 { + fn transfer_and_call( + ref self: TContractState, to: ContractAddress, value: u256, data: Array + ) -> bool; +} + +#[starknet::component] +mod ERC677Component { + use starknet::ContractAddress; + use openzeppelin::token::erc20::interface::IERC20; + use openzeppelin::introspection::interface::{ISRC5, ISRC5Dispatcher, ISRC5DispatcherTrait}; + use array::ArrayTrait; + use array::SpanTrait; + use clone::Clone; + use array::ArrayTCloneImpl; + use chainlink::libraries::token::v2::erc677_receiver::{ + IERC677ReceiverDispatcher, IERC677ReceiverDispatcherTrait, IERC677_RECEIVER_ID + }; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + TransferAndCall: TransferAndCall, + } + + #[derive(Drop, starknet::Event)] + struct TransferAndCall { + #[key] + from: ContractAddress, + #[key] + to: ContractAddress, + value: u256, + data: Array + } + + #[embeddable_as(ERC677Impl)] + impl ERC677< + TContractState, + +HasComponent, + +IERC20, + +Drop, + > of super::IERC677> { + fn transfer_and_call( + ref self: ComponentState, + to: ContractAddress, + value: u256, + data: Array + ) -> bool { + let sender = starknet::info::get_caller_address(); + + let mut contract = self.get_contract_mut(); + contract.transfer(to, value); + self + .emit( + Event::TransferAndCall( + TransferAndCall { from: sender, to: to, value: value, data: data.clone(), } + ) + ); + + let receiver = ISRC5Dispatcher { contract_address: to }; + + let supports = receiver.supports_interface(IERC677_RECEIVER_ID); + + if supports { + IERC677ReceiverDispatcher { contract_address: to } + .on_token_transfer(sender, value, data); + } + true + } + } +} diff --git a/contracts/src/libraries/token/v2/erc677_receiver.cairo b/contracts/src/libraries/token/v2/erc677_receiver.cairo new file mode 100644 index 000000000..11249d037 --- /dev/null +++ b/contracts/src/libraries/token/v2/erc677_receiver.cairo @@ -0,0 +1,43 @@ +use starknet::ContractAddress; + +const IERC677_RECEIVER_ID: felt252 = + 0x224f0246bc4ebdcc391196e93f522342f12393c1a456db2a57043638940254; + +#[starknet::interface] +trait IERC677Receiver { + fn on_token_transfer( + ref self: TContractState, sender: ContractAddress, value: u256, data: Array + ); +} + +#[starknet::component] +mod ERC677ReceiverComponent { + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; + use starknet::ContractAddress; + use super::{IERC677Receiver, IERC677_RECEIVER_ID}; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, + +HasComponent, + // ensure that the contract implements the IERC677Receiver interface + +IERC677Receiver, + impl SRC5: SRC5Component::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the contract by registering the IERC677Receiver interface ID. + /// This should be used inside the contract's constructor. + fn initializer(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(IERC677_RECEIVER_ID); + } + } +} diff --git a/contracts/src/libraries/token/v2/test.cairo b/contracts/src/libraries/token/v2/test.cairo new file mode 100644 index 000000000..04c23ff4a --- /dev/null +++ b/contracts/src/libraries/token/v2/test.cairo @@ -0,0 +1,8 @@ +trait IERC677Receiver { + fn on_token_transfer(sender: ContractAddress, value: u256, data: Array); +} + +trait IERC677 { + fn transfer_and_call(to: ContractAddress, value: u256, data: Array) -> bool; +} + diff --git a/contracts/src/tests/test_aggregator.cairo b/contracts/src/tests/test_aggregator.cairo index eecca9a88..bed9dad22 100644 --- a/contracts/src/tests/test_aggregator.cairo +++ b/contracts/src/tests/test_aggregator.cairo @@ -21,9 +21,12 @@ use chainlink::ocr2::aggregator::Aggregator::{ use chainlink::ocr2::aggregator::Aggregator::BillingConfig; use chainlink::ocr2::aggregator::Aggregator::PayeeConfig; use chainlink::access_control::access_controller::AccessController; -use chainlink::token::link_token::LinkToken; -use chainlink::tests::test_ownable::should_implement_ownable; -use chainlink::tests::test_access_controller::should_implement_access_control; +use chainlink::token::v2::link_token::LinkToken; +use chainlink::tests::{ + test_ownable::should_implement_ownable, test_access_controller::should_implement_access_control, + test_link_token::link_deploy_args +}; + use snforge_std::{ declare, ContractClassTrait, start_cheat_caller_address_global, stop_cheat_caller_address_global @@ -97,10 +100,7 @@ fn setup() -> ( contract_address: billingAccessControllerAddr }; - // deploy link token contract - let calldata = array![acc1.into(), // minter = acc1; - acc1.into(), // owner = acc1; - ]; + let calldata = link_deploy_args(acc1, acc1); let (linkTokenAddr, _) = declare("LinkToken").unwrap().deploy(@calldata).unwrap(); diff --git a/contracts/src/tests/test_erc677.cairo b/contracts/src/tests/test_erc677.cairo index b81394a21..128d8c15d 100644 --- a/contracts/src/tests/test_erc677.cairo +++ b/contracts/src/tests/test_erc677.cairo @@ -13,8 +13,8 @@ use core::result::ResultTrait; use chainlink::token::mock::valid_erc667_receiver::ValidReceiver; use chainlink::token::mock::invalid_erc667_receiver::InvalidReceiver; -use chainlink::libraries::token::erc677::ERC677Component; -use chainlink::libraries::token::erc677::ERC677Component::ERC677Impl; +use chainlink::libraries::token::v2::erc677::ERC677Component; +use chainlink::libraries::token::v2::erc677::ERC677Component::ERC677Impl; use snforge_std::{ declare, ContractClassTrait, start_cheat_caller_address_global, stop_cheat_caller_address_global @@ -58,7 +58,7 @@ fn setup_invalid_receiver() -> (ContractAddress, MockInvalidReceiverDispatcher) } type ComponentState = - ERC677Component::ComponentState; + ERC677Component::ComponentState; fn transfer_and_call(receiver: ContractAddress) { let data = ArrayTrait::::new(); diff --git a/contracts/src/tests/test_link_token.cairo b/contracts/src/tests/test_link_token.cairo index 1ee0f9946..7e6f9a923 100644 --- a/contracts/src/tests/test_link_token.cairo +++ b/contracts/src/tests/test_link_token.cairo @@ -1,19 +1,17 @@ -use starknet::ContractAddress; -use starknet::testing::set_caller_address; -use starknet::contract_address_const; -use starknet::class_hash::class_hash_const; -use starknet::class_hash::Felt252TryIntoClassHash; -use starknet::syscalls::deploy_syscall; +use starknet::{ + syscalls::deploy_syscall, ContractAddress, testing::set_caller_address, contract_address_const, + class_hash::{class_hash_const, Felt252TryIntoClassHash} +}; use array::ArrayTrait; -use traits::Into; -use traits::TryInto; +use traits::{Into, TryInto}; use zeroable::Zeroable; use option::OptionTrait; use core::result::ResultTrait; -use chainlink::token::link_token::LinkToken; -use chainlink::token::link_token::LinkToken::{MintableToken, UpgradeableImpl}; +use chainlink::token::v2::link_token::{ + LinkToken, LinkToken::{MintableToken, UpgradeableImpl, Minter} +}; use openzeppelin::token::erc20::ERC20Component::{ERC20Impl, ERC20MetadataImpl}; use chainlink::tests::test_ownable::should_implement_ownable; @@ -36,13 +34,33 @@ fn setup() -> ContractAddress { account } +fn link_deploy_args(minter: ContractAddress, owner: ContractAddress) -> Array { + let mut calldata = ArrayTrait::new(); + let _name_ignore: felt252 = 0; + let _symbol_ignore: felt252 = 0; + let _decimals_ignore: u8 = 0; + let _initial_supply_ignore: u256 = 0; + let _initial_recipient_ignore: ContractAddress = Zeroable::zero(); + let _upgrade_delay_ignore: u64 = 0; + Serde::serialize(@_name_ignore, ref calldata); + Serde::serialize(@_symbol_ignore, ref calldata); + Serde::serialize(@_decimals_ignore, ref calldata); + Serde::serialize(@_initial_supply_ignore, ref calldata); + Serde::serialize(@_initial_recipient_ignore, ref calldata); + Serde::serialize(@minter, ref calldata); + Serde::serialize(@owner, ref calldata); + Serde::serialize(@_upgrade_delay_ignore, ref calldata); + + calldata +} + #[test] fn test_ownable() { let account = setup(); // Deploy LINK token - let mut calldata = ArrayTrait::new(); - calldata.append(class_hash_const::<123>().into()); // minter - calldata.append(account.into()); // owner + let calldata = link_deploy_args(contract_address_const::<123>(), // minter + account // owner + ); let (linkAddr, _) = declare("LinkToken").unwrap().deploy(@calldata).unwrap(); @@ -55,16 +73,16 @@ fn test_constructor_zero_address() { let sender = setup(); let mut state = STATE(); - LinkToken::constructor(ref state, Zeroable::zero(), sender); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), Zeroable::zero(), sender, 0); } #[test] fn test_constructor() { let sender = setup(); let mut state = STATE(); - LinkToken::constructor(ref state, sender, sender); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), sender, sender, 0); - assert(LinkToken::minter(@state) == sender, 'minter valid'); + assert(Minter::minter(@state) == sender, 'minter valid'); assert(state.erc20.name() == "ChainLink Token", 'name valid'); assert(state.erc20.symbol() == "LINK", 'symbol valid'); } @@ -73,7 +91,7 @@ fn test_constructor() { fn test_permissioned_mint_from_minter() { let sender = setup(); let mut state = STATE(); - LinkToken::constructor(ref state, sender, sender); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), sender, sender, 0); let to = contract_address_const::<908>(); let zero: felt252 = 0; @@ -93,7 +111,7 @@ fn test_permissioned_mint_from_nonminter() { let sender = setup(); let mut state = STATE(); let minter = contract_address_const::<111>(); - LinkToken::constructor(ref state, minter, sender); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), minter, sender, 0); let to = contract_address_const::<908>(); let amount: felt252 = 3000; @@ -106,7 +124,7 @@ fn test_permissioned_burn_from_minter() { let zero = 0; let sender = setup(); let mut state = STATE(); - LinkToken::constructor(ref state, sender, sender); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), sender, sender, 0); let to = contract_address_const::<908>(); let amount: felt252 = 3000; @@ -134,7 +152,7 @@ fn test_permissioned_burn_from_nonminter() { let sender = setup(); let mut state = STATE(); let minter = contract_address_const::<111>(); - LinkToken::constructor(ref state, minter, sender); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), minter, sender, 0); let to = contract_address_const::<908>(); let amount: felt252 = 3000; @@ -146,8 +164,48 @@ fn test_permissioned_burn_from_nonminter() { fn test_upgrade_non_owner() { let sender = setup(); let mut state = STATE(); - LinkToken::constructor(ref state, sender, contract_address_const::<111>()); + LinkToken::constructor( + ref state, 0, 0, 0, 0, Zeroable::zero(), sender, contract_address_const::<111>(), 0 + ); UpgradeableImpl::upgrade(ref state, class_hash_const::<123>()); } +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_minter_non_owner() { + let sender = setup(); + let mut state = STATE(); + LinkToken::constructor( + ref state, 0, 0, 0, 0, Zeroable::zero(), sender, contract_address_const::<111>(), 0 + ); + + Minter::set_minter(ref state, contract_address_const::<123>()) +} + +#[test] +#[should_panic(expected: ('is minter already',))] +fn test_set_minter_already() { + let sender = setup(); + let mut state = STATE(); + + let minter = contract_address_const::<111>(); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), minter, sender, 0); + + Minter::set_minter(ref state, minter); +} + +#[test] +fn test_set_minter_success() { + let sender = setup(); + let mut state = STATE(); + + let minter = contract_address_const::<111>(); + LinkToken::constructor(ref state, 0, 0, 0, 0, Zeroable::zero(), minter, sender, 0); + + let new_minter = contract_address_const::<222>(); + Minter::set_minter(ref state, new_minter); + + assert(new_minter == Minter::minter(@state), 'new minter should be 222'); +} + diff --git a/contracts/src/token.cairo b/contracts/src/token.cairo index 1c56c11b7..924d36d77 100644 --- a/contracts/src/token.cairo +++ b/contracts/src/token.cairo @@ -1,2 +1,3 @@ -mod link_token; +mod v1; +mod v2; mod mock; diff --git a/contracts/src/token/mock/invalid_erc667_receiver.cairo b/contracts/src/token/mock/invalid_erc667_receiver.cairo index 9332dc66f..34a48bf47 100644 --- a/contracts/src/token/mock/invalid_erc667_receiver.cairo +++ b/contracts/src/token/mock/invalid_erc667_receiver.cairo @@ -18,7 +18,7 @@ mod InvalidReceiver { } #[external(v0)] - fn supports_interface(self: @ContractState, interface_id: u32) -> bool { + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { self._supports.read() } } diff --git a/contracts/src/token/mock/valid_erc667_receiver.cairo b/contracts/src/token/mock/valid_erc667_receiver.cairo index cd08f7da6..285c8532c 100644 --- a/contracts/src/token/mock/valid_erc667_receiver.cairo +++ b/contracts/src/token/mock/valid_erc667_receiver.cairo @@ -8,28 +8,51 @@ trait MockValidReceiver { mod ValidReceiver { use starknet::ContractAddress; use array::ArrayTrait; - use chainlink::libraries::token::erc677::IERC677Receiver; + use openzeppelin::introspection::src5::SRC5Component; + use chainlink::libraries::token::v2::erc677_receiver::{ + ERC677ReceiverComponent, IERC677Receiver + }; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: ERC677ReceiverComponent, storage: erc677_receiver, event: ERC677ReceiverEvent); + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + // ERC677Receiver + impl SRC5InternalImpl = ERC677ReceiverComponent::InternalImpl; #[storage] struct Storage { _sender: ContractAddress, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + erc677_receiver: ERC677ReceiverComponent::Storage } - #[constructor] - fn constructor(ref self: ContractState) {} + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + ERC677ReceiverEvent: ERC677ReceiverComponent::Event + } + #[constructor] + fn constructor(ref self: ContractState) { + self.erc677_receiver.initializer(); + } #[abi(embed_v0)] - impl ERC677Receiver of IERC677Receiver { + impl ERC677ReceiverImpl of IERC677Receiver { fn on_token_transfer( ref self: ContractState, sender: ContractAddress, value: u256, data: Array ) { self._sender.write(sender); } - - fn supports_interface(ref self: ContractState, interface_id: u32) -> bool { - true - } } #[abi(embed_v0)] diff --git a/contracts/src/token/v1.cairo b/contracts/src/token/v1.cairo new file mode 100644 index 000000000..c62e1f2e7 --- /dev/null +++ b/contracts/src/token/v1.cairo @@ -0,0 +1 @@ +mod link_token; diff --git a/contracts/src/token/link_token.cairo b/contracts/src/token/v1/link_token.cairo similarity index 98% rename from contracts/src/token/link_token.cairo rename to contracts/src/token/v1/link_token.cairo index 24bdb2271..6a6df4a18 100644 --- a/contracts/src/token/link_token.cairo +++ b/contracts/src/token/v1/link_token.cairo @@ -19,7 +19,7 @@ mod LinkToken { use super::IMintableToken; use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; - use chainlink::libraries::token::erc677::ERC677Component; + use chainlink::libraries::token::v1::erc677::ERC677Component; use chainlink::libraries::type_and_version::ITypeAndVersion; use chainlink::libraries::upgradeable::{Upgradeable, IUpgradeable}; diff --git a/contracts/src/token/v2.cairo b/contracts/src/token/v2.cairo new file mode 100644 index 000000000..c62e1f2e7 --- /dev/null +++ b/contracts/src/token/v2.cairo @@ -0,0 +1 @@ +mod link_token; diff --git a/contracts/src/token/v2/link_token.cairo b/contracts/src/token/v2/link_token.cairo new file mode 100644 index 000000000..638d96bbd --- /dev/null +++ b/contracts/src/token/v2/link_token.cairo @@ -0,0 +1,177 @@ +use starknet::ContractAddress; + +// This token is deployed by the StarkGate bridge + +// https://github.com/starknet-io/starkgate-contracts/blob/v2.0/src/cairo/mintable_token_interface.cairo +#[starknet::interface] +trait IMintableToken { + fn permissioned_mint(ref self: TContractState, account: ContractAddress, amount: u256); + fn permissioned_burn(ref self: TContractState, account: ContractAddress, amount: u256); +} + +// allows setting and getting the minter +#[starknet::interface] +trait IMinter { + fn set_minter(ref self: TContractState, new_minter: ContractAddress); + fn minter(self: @TContractState) -> ContractAddress; +} + +#[starknet::contract] +mod LinkToken { + use starknet::{contract_address_const, ContractAddress, class_hash::ClassHash}; + use zeroable::Zeroable; + use openzeppelin::{ + token::erc20::{ + ERC20Component, interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait} + }, + access::ownable::OwnableComponent + }; + use super::{IMintableToken, IMinter}; + use chainlink::libraries::{ + token::v2::erc677::ERC677Component, type_and_version::ITypeAndVersion, + upgradeable::{Upgradeable, IUpgradeable} + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: ERC677Component, storage: erc677, event: ERC677Event); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl; + impl InternalImpl = OwnableComponent::InternalImpl; + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[abi(embed_v0)] + impl ERC677Impl = ERC677Component::ERC677Impl; + + #[storage] + struct Storage { + LinkTokenV2_minter: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + erc677: ERC677Component::Storage, + } + + #[derive(Drop, starknet::Event)] + struct LinkTokenV2NewMinter { + old_minter: ContractAddress, + new_minter: ContractAddress + } + + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + LinkTokenV2NewMinter: LinkTokenV2NewMinter, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + ERC677Event: ERC677Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + _name_ignore: felt252, + _symbol_ignore: felt252, + _decimals_ignore: u8, + _initial_supply_ignore: u256, + _initial_recipient_ignore: ContractAddress, + initial_minter: ContractAddress, + owner: ContractAddress, + _upgrade_delay_ignore: u64 + ) { + let name = "ChainLink Token"; + let symbol = "LINK"; + + self.erc20.initializer(name, symbol); + self.ownable.initializer(owner); + + assert(!initial_minter.is_zero(), 'minter is 0'); + self.LinkTokenV2_minter.write(initial_minter); + + self + .emit( + Event::LinkTokenV2NewMinter( + LinkTokenV2NewMinter { + old_minter: contract_address_const::<0>(), new_minter: initial_minter + } + ) + ); + } + + + #[abi(embed_v0)] + impl MintableToken of IMintableToken { + fn permissioned_mint(ref self: ContractState, account: ContractAddress, amount: u256) { + self._only_minter(); + self.erc20._mint(account, amount); + } + + fn permissioned_burn(ref self: ContractState, account: ContractAddress, amount: u256) { + self._only_minter(); + self.erc20._burn(account, amount); + } + } + + #[abi(embed_v0)] + impl Minter of IMinter { + fn set_minter(ref self: ContractState, new_minter: ContractAddress) { + self.ownable.assert_only_owner(); + + let prev_minter = self.LinkTokenV2_minter.read(); + assert(new_minter != prev_minter, 'is minter already'); + + self.LinkTokenV2_minter.write(new_minter); + + self + .emit( + Event::LinkTokenV2NewMinter( + LinkTokenV2NewMinter { old_minter: prev_minter, new_minter: new_minter } + ) + ); + } + + fn minter(self: @ContractState) -> ContractAddress { + self.LinkTokenV2_minter.read() + } + } + + #[abi(embed_v0)] + impl TypeAndVersionImpl of ITypeAndVersion { + fn type_and_version(self: @ContractState) -> felt252 { + 'LinkToken 2.0.0' + } + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + fn upgrade(ref self: ContractState, new_impl: ClassHash) { + self.ownable.assert_only_owner(); + Upgradeable::upgrade(new_impl) + } + } + + // + // Internal + // + + #[generate_trait] + impl InternalFunctions of InternalFunctionsTrait { + fn _only_minter(self: @ContractState) { + let caller = starknet::get_caller_address(); + let minter = self.LinkTokenV2_minter.read(); + assert(caller == minter, 'only minter'); + } + } +}